I am working on the file "Lectures/Lecture06 -- Radiation.ipynb".
The first task is:
Note that surface temperature in global mean is indeed about 288 K or 15ºC as we keep saying.
So where do we find temperature $T_e=255$ K or -18ºC?
Actually in mid-troposphere, near 500 hPa or about 5 km height.
We can infer that much of the outgoing longwave radiation actually originates far above the surface.
Recall that our observed global energy budget diagram shows 217 out of 239 W m$^{-2}$ total OLR emitted by the atmosphere and clouds, only 22 W m$^{-2}$ directly from the surface.
This is due to the greenhouse effect.
So far we have dealt with the greenhouse in a very artificial way in our energy balance model by simply assuming
$$ \text{OLR} = \tau \sigma T_s^4 $$
i.e., the OLR is reduced by a constant factor from the value it would have if the Earth emitted as a blackbody at the surface temperature.
Now it's time to start thinking a bit more about how the radiative transfer process actually occurs in the atmosphere, and how to model it.

2. Solar Radiation

Let's plot a spectrum of solar radiation.
For details, see code in notebook!
Can you write Python code for it?

#  Using pre-defined code for the Planck function from the climlab package
from climlab.utils.thermo import Planck_wavelength

# approximate emission temperature of the sun in Kelvin
Tsun = 5780.
# boundaries of visible region in nanometers
UVbound = 390.
IRbound = 700.
#  array of wavelengths
wavelength_nm = np.linspace(0.1, 3500, 400)
to_meters = 1E-9  # conversion factor

label_size = 16
fig, ax = plt.subplots(figsize=(14,7))
ax.plot(wavelength_nm,
         Planck_wavelength(wavelength_nm * to_meters, Tsun))
ax.grid()
ax.set_xlabel('Wavelength (nm)', fontsize=label_size)
ax.set_ylabel('Spectral radiance (W sr$^{-1}$ m$^{-3}$)', fontsize=label_size)
#  Mask out points outside of this range
wavelength_vis = np.ma.masked_outside(wavelength_nm, UVbound, IRbound)
#  Shade the visible region
ax.fill_between(wavelength_vis, Planck_wavelength(wavelength_vis * to_meters, Tsun))
title = 'Blackbody emission curve for the sun (T = {:.0f} K)'.format(Tsun)
ax.set_title(title, fontsize=label_size);
ax.text(280, 0.8E13, 'Ultraviolet', rotation='vertical', fontsize=12)
ax.text(500, 0.8E13, 'Visible', rotation='vertical', fontsize=16, color='w')
ax.text(800, 0.8E13, 'Infrared', rotation='vertical', fontsize=12);


I am working on the file "Lectures/Lecture06 -- Radiation.ipynb".
The first task is:
There is essentially no overlap between the two spectra.
This is the fundamental reason we can discuss the solar “shortwave” and terrestrial “longwave” radiation as two distinct phenomena.
In reality all radiation exists on a continuum of different wavelengths. But in climate science we can get a long way by thinking in terms of a very simple “two-stream” approximation (short and longwave). We’ve already been doing this throughout the course so far!
Atmospheric absorption spectra
Now look at the atmospheric absorption spectra.
(fraction of radiation at each wavelength that is absorbed on a single vertical path through the atmosphere)

Figure reproduced from Marshall and Plumb (2008): Atmosphere, Ocean, and Climate Dynamics

Atmosphere is almost completely transparent in the visible range, right at the peak of the solar spectrum
Atmosphere is very opaque in the UV
Opacity across the IR spectrum is highly variable!
Look at the gases associated with various absorption features:
Main players include H$_2$O, CO$_2$, N$_2$O, O$_2$.

Compare to major constituents of atmosphere, in decreasing order:

78% N$_2$
21% O$_2$
1% Ar
H$_2$O (variable)

The dominant constituent gases N$_2$ and O$_2$ are nearly completely transparent across the entire spectrum (there are O$_2$ absorption features in far UV, but little energy at these wavelengths).

The greenhouse effect mostly involves trace constituents:
O$_3$ = 500 ppb
N$_2$O = 310 ppb
CO$_2$ = 400 ppm (but rapidly increasing!)
CH$_4$ = 1.7 ppm

Note that most of these are tri-atomic molecules! There are fundamental reasons for this: these molecules have modes of rotational and vibration that are easily excited at IR wavelengths. See courses in radiative transfer!

[Back to ATM 623 notebook home](../index.ipynb)

Version information
Can you write Python code for it?

%load_ext version_information
%version_information numpy, matplotlib, xarray, metpy, climlab


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:

4. Level of emission

Even in this very simple greenhouse model, there is no single level at which the OLR is generated.
The three terms in our formula for OLR tell us the contributions from each level.
Can you write Python code for it?

OLRterms = sympy.Matrix([OLR_s, OLR_0, OLR_1])
OLRterms


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
After the perturbation we have
Can you write Python code for it?

delta_epsilon = sympy.symbols('delta_epsilon')
OLRterms_pert = OLRterms.subs(epsilon, epsilon+delta_epsilon)
OLRterms_pert


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
And the net radiative forcing is
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Between layer 1 and layer 0 the beam contains emissions from layer 1:
$$ D_1 = (1-\epsilon)D_2 + E_1 = E_1 $$
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Contents

A single layer atmosphere
Introducing the two-layer grey gas model
Tuning the grey gas model to observations
Level of emission
Radiative forcing in the 2-layer grey gas model
Radiative equilibrium in the 2-layer grey gas model
Summary

1. A single layer atmosphere

We will make our first attempt at quantifying the greenhouse effect in the simplest possible greenhouse model: a single layer of atmosphere that is able to absorb and emit longwave radiation.

Assumptions

Atmosphere is a single layer of air at temperature $T_a$
Atmosphere is completely transparent to shortwave solar radiation.
The surface absorbs shortwave radiation $(1-\alpha) Q$
Atmosphere is completely opaque to infrared radiation
Both surface and atmosphere emit radiation as blackbodies ($\sigma T_s^4, \sigma T_a^4$)
Atmosphere radiates equally up and down ($\sigma T_a^4$)
There are no other heat transfer mechanisms

We can now use the concept of energy balance to ask what the temperature need to be in order to balance the energy budgets at the surface and the atmosphere, i.e. the radiative equilibrium temperatures.
Energy balance at the surface
\begin{align}
\text{energy in} &= \text{energy out} \
(1-\alpha) Q + \sigma T_a^4 &= \sigma T_s^4 \
\end{align}
The presence of the atmosphere above means there is an additional source term: downwelling infrared radiation from the atmosphere.
We call this the back radiation.
Energy balance for the atmosphere
\begin{align}
\text{energy in} &= \text{energy out} \
\sigma T_s^4 &= A\uparrow + A\downarrow = 2 \sigma T_a^4 \
\end{align}
which means that
$$ T_s = 2^\frac{1}{4} T_a \approx 1.2 T_a $$
So we have just determined that, in order to have a purely radiative equilibrium, we must have $T_s > T_a$.
The surface must be warmer than the atmosphere.
Solve for the radiative equilibrium surface temperature
Now plug this into the surface equation to find
$$ \frac{1}{2} \sigma T_s^4 = (1-\alpha) Q  $$
and use the definition of the...
Can you write Python code for it?

import sympy
#  Allow sympy to produce nice looking equations as output
sympy.init_printing()
#  Define some symbols for mathematical quantities
#  Assume all quantities are positive (which will help simplify some expressions)
epsilon, T_e, T_s, T_0, T_1, sigma = \
    sympy.symbols('epsilon, T_e, T_s, T_0, T_1, sigma', positive=True)
#  So far we have just defined some symbols, e.g.
T_s

#  We have hard-coded the assumption that the temperature is positive
sympy.ask(T_s>0)


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Continuing to follow the same beam, the upwelling flux above layer 1 is
$$ U_2 = (1-\epsilon) U_1 + E_1 $$
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Downwelling beam
Let $D$ be the downwelling longwave beam. Since there is no longwave radiation coming in from space, we begin with
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Finally between layer 0 and the surface the beam contains a transmitted component and the emissions from layer 0:
$$ D_0 = (1-\epsilon) D_1 + E_0 = \epsilon(1-\epsilon) \sigma T_1^4 + \epsilon \sigma T_0^4$$
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
In more familiar notation, the radiative equilibrium solution is thus
\begin{align}
T_s &= T_e \left( \frac{2+\epsilon}{2-\epsilon} \right)^{1/4} \
T_0 &= T_e \left( \frac{1+\epsilon}{2-\epsilon} \right)^{1/4} \
T_1 &= T_e \left( \frac{ 1}{2 - \epsilon} \right)^{1/4}
\end{align}
Plugging in the tuned value $\epsilon = 0.586$ gives
Can you write Python code for it?

Tsolution = solution.subs(tuned)
#  Display result as matrix equation!
sympy.Equality(T, Tsolution)


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Is this a positive or negative number? The key point is this:
It depends on the temperatures, i.e. on the lapse rate.
Greenhouse effect for an isothermal atmosphere
Stop and think about this question:
If the surface and atmosphere are all at the same temperature, does the OLR go up or down when $\epsilon$ increases (i.e. we add more absorbers)?
Understanding this question is key to understanding how the greenhouse effect works.
Let's solve the isothermal case
We will just set $T_s = T_0 = T_1$ in the above expression for the radiative forcing.
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Just as we did for the 1-layer model, it is helpful to rewrite this system using the definition of the emission temperture $T_e$
$$ (1-\alpha) Q = \sigma T_e^4 $$
Can you write Python code for it?

radeq2 = radeq.subs([((1-alpha)*Q, sigma*T_e**4)])
radeq2


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Longwave emissions
Let's denote the emissions from each layer as
\begin{align}
E_s &= \sigma T_s^4 \
E_0 &= \epsilon \sigma T_0^4 \
E_1 &= \epsilon \sigma T_1^4
\end{align}
recognizing that $E_0$ and $E_1$ contribute to both the upwelling and downwelling beams.
Can you write Python code for it?

#  Define these operations as sympy symbols
#  And display as a column vector:
E_s = sigma*T_s**4
E_0 = epsilon*sigma*T_0**4
E_1 = epsilon*sigma*T_1**4
E = sympy.Matrix([E_s, E_0, E_1])
E


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Net absorption is the flux convergence in each layer
(difference between what's coming in the bottom and what's going out the top of each layer)
Can you write Python code for it?

# define a vector of absorbed radiation -- same size as emissions
A = E.copy()

#  absorbed radiation at surface
A[0] = F[0]
# Compute the convergence
for n in range(2):
    A[n+1] = -(F[n+1]-F[n])

A


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Notice that the largest single contribution is coming from the top layer. This is in spite of the fact that the emissions from this layer are weak, because it is so cold.
Comparing to observations, the actual contribution to OLR from the surface is about 22 W m$^{-2}$ (or about 9% of the total), not 67 W m$^{-2}$. So we certainly don't have all the details worked out yet!
As we will see later, to really understand what sets that observed 22 W m$^{-2}$, we will need to start thinking about the spectral dependence of the longwave absorptivity.

5. Radiative forcing in the 2-layer grey gas model

Adding some extra greenhouse absorbers will mean that a greater fraction of incident longwave radiation is absorbed in each layer.
Thus $\epsilon$ must increase as we add greenhouse gases.
Suppose we have $\epsilon$ initially, and the absorptivity increases to $\epsilon_2 = \epsilon + \delta_\epsilon$.
Suppose further that this increase happens abruptly so that there is no time for the temperatures to respond to this change. We hold the temperatures fixed in the column and ask how the radiative fluxes change.
Do you expect the OLR to increase or decrease?
Let's use our two-layer leaky greenhouse model to investigate the answer.
The components of the OLR before the perturbation are
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Now look at the sign of each term. Recall that $0 < \epsilon < 1$. Which terms in the OLR go up and which go down?
THIS IS VERY IMPORTANT, SO STOP AND THINK ABOUT IT.
The contribution from the surface must decrease, while the contribution from the top layer must increase.
When we add absorbers, the average level of emission goes up!
"Radiative forcing" is the change in radiative flux at TOA after adding absorbers
In this model, only the longwave flux can change, so we define the radiative forcing as
$$ R = - \delta OLR $$
(with the minus sign so that $R$ is positive when the climate system is gaining extra energy).
We just worked out that whenever we add some extra absorbers, the emissions to space (on average) will originate from higher levels in the atmosphere.
What does this mean for OLR? Will it increase or decrease?
To get the answer, we just have to sum up the three contributions we wrote above:
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Now we just need to know the Earth's emission temperature $T_e$!
(Which we already know is about 255 K)
Can you write Python code for it?

# Here's how to calculate T_e from the observed values
sympy.solve(((1-alpha)*Q - sigma*T_e**4).subs(tuned), T_e)

# Need to unpack the list
Te_value = sympy.solve(((1-alpha)*Q - sigma*T_e**4).subs(tuned), T_e)[0]
Te_value


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Radiative equilibrium means net absorption is ZERO in the atmosphere
The only other heat source is the shortwave heating at the surface.
In matrix form, here is the system of equations to be solved:
Can you write Python code for it?

radeq = sympy.Equality(A, sympy.Matrix([(1-alpha)*Q, 0, 0]))
radeq


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Recall that the three terms are the contributions to the OLR from the three different levels. In this case, the changes in those contributions after adding more absorbers.
Now let's divide through by $\delta_\epsilon$ to get the normalized change in OLR per unit change in absorptivity:
Can you write Python code for it?

deltaOLR_per_deltaepsilon = \
    sympy.simplify(deltaOLR_linear / delta_epsilon)
deltaOLR_per_deltaepsilon


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Shortwave radiation
Since we have assumed the atmosphere is transparent to shortwave, the incident beam $Q$ passes unchanged from the top to the surface, where a fraction $\alpha$ is reflected upward out to space.
Can you write Python code for it?

#  Define some new symbols for shortwave radiation
Q, alpha = sympy.symbols('Q, alpha', positive=True)
#  Create a dictionary to hold our numerical values
tuned = {}
tuned[Q] = 341.3  #  global mean insolation in W/m2
tuned[alpha] = 101.9/Q.subs(tuned)  #  observed planetary albedo
tuned[sigma] = 5.67E-8  #  Stefan-Boltzmann constant in W/m2/K4
tuned
#  Numerical value for emission temperature
#T_e.subs(tuned)


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Since there is no more atmosphere above layer 1, this upwelling flux is our Outgoing Longwave Radiation for this model:
$$ OLR = U_2 $$
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
So we are getting about 67 W m$^{-2}$ from the surface, 79 W m$^{-2}$ from layer 0, and 93 W m$^{-2}$ from the top layer.
In terms of fractional contributions to the total OLR, we have (limiting the output to two decimal places):
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
The answer is zero
For an isothermal atmosphere, there is no change in OLR when we add extra greenhouse absorbers. Hence, no radiative forcing and no greenhouse effect.
Why?
The level of emission still must go up. But since the temperature at the upper level is the same as everywhere else, the emissions are exactly the same.
The radiative forcing (change in OLR) depends on the lapse rate!
For a more realistic example of radiative forcing due to an increase in greenhouse absorbers, we can substitute in our tuned values for temperature and $\epsilon$.
We'll express the answer in W m$^{-2}$ for a 1% increase in $\epsilon$.
The three components of the OLR change are
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
The radiative equilibrium solution is substantially warmer at the surface and colder in the lower troposphere than reality.
This is a very general feature of radiative equilibrium, and we will see it again very soon in this course.

7. Summary

Key physical lessons

Putting a layer of longwave absorbers above the surface keeps the surface substantially warmer, because of the backradiation from the atmosphere (greenhouse effect).

The grey gas model assumes that each layer absorbs and emits a fraction $\epsilon$ of its blackbody value, independent of wavelength.

With incomplete absorption ($\epsilon < 1$), there are contributions to the OLR from every level and the surface (there is no single level of emission)

Adding more absorbers means that contributions to the OLR from upper levels go up, while contributions from the surface go down.

This upward shift in the weighting of different levels is what we mean when we say the level of emission goes up.

The radiative forcing caused by an increase in absorbers depends on the lapse rate.

For an isothermal atmosphere the radiative forcing is zero and there is no greenhouse effect
The radiative forcing is positive for our atmosphere because tropospheric temperatures tends to decrease with height.
Pure radiative equilibrium produces a warm surface and cold lower troposphere.
This is unrealistic, and suggests that crucial heat transfer mechanisms are missing from our model.

And on the Python side...
Did we need sympy to work all this out? No, of course not. We could have solved the 3x3 matrix problems by hand. But computer algebra can be very useful and save you a lot of time and error, so it's good to invest some effort into learning how to use it.
Hopefully these notes provide a useful starting point.
A follow-up assignment
You are now ready to tackle Assignment 5, where you are asked to extend this grey-gas analysis to many layers.
For more than a few layers, the analytical approach we used here is no longer very useful. You will code...
Can you write Python code for it?

%load_ext version_information
%version_information sympy


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
The three terms in the above expression represent the contributions to the total OLR that originate from each of the three levels.
Let's code this up explicitly for future reference:
Can you write Python code for it?

#  Define the contributions to OLR originating from each level
OLR_s = (1-epsilon)**2 *sigma*T_s**4
OLR_0 = epsilon*(1-epsilon)*sigma*T_0**4
OLR_1 = epsilon*sigma*T_1**4

OLR = OLR_s + OLR_0 + OLR_1

print( 'The expression for OLR is')
OLR


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
There are two roots, but the second one is unphysical since we must have $0 < \epsilon < 1$.
Just for fun, here is a simple of example of filtering a list using powerful Python list comprehension syntax:
Can you write Python code for it?

# Give me only the roots that are between zero and 1!
list_result = [eps for eps in eps_solution if 0<eps<1]
print( list_result)
# The result is a list with a single element.
#  We need to slice the list to get just the number:
eps_tuned = list_result[0]
print( eps_tuned)


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
This $D_0$ is what we call the back radiation, i.e. the longwave radiation from the atmosphere to the surface.

3. Tuning the grey gas model to observations

In building our new model we have introduced exactly one parameter, the absorptivity $\epsilon$. We need to choose a value for $\epsilon$.
We will tune our model so that it reproduces the observed global mean OLR given observed global mean temperatures.
To get appropriate temperatures for $T_s, T_0, T_1$, let's revisit the global, annual mean lapse rate plot from NCEP Reanalysis data from the previous lecture.
Temperatures
First, we set
$$T_s = 288 \text{ K}  $$
From the lapse rate plot, an average temperature for the layer between 1000 and 500 hPa is
$$ T_0 = 275 \text{ K}$$
Defining an average temperature for the layer between 500 and 0 hPa is more ambiguous because of the lapse rate reversal at the tropopause. We will choose
$$ T_1 = 230 \text{ K}$$
From the graph, this is approximately the observed global mean temperature at 275 hPa or about 10 km.
Can you write Python code for it?

#  add to our dictionary of values:
tuned[T_s] = 288.
tuned[T_0] = 275.
tuned[T_1] = 230.
tuned


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
This produces a dictionary of solutions for the fourth power of the temperatures!
A little manipulation gets us the solutions for temperatures that we want:
Can you write Python code for it?

# need the symbolic fourth root operation
from sympy.simplify.simplify import nthroot

fourthpower_list = [fourthpower[key] for key in [T_s**4, T_0**4, T_1**4]]
solution = sympy.Matrix([nthroot(item,4) for item in fourthpower_list])
#  Display result as matrix equation!
T = sympy.Matrix([T_s, T_0, T_1])
sympy.Equality(T, solution)


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
To make things simpler, we will neglect the terms in $\delta_\epsilon^2$. This is perfectly reasonably because we are dealing with small perturbations where $\delta_\epsilon << \epsilon$.
Telling sympy to set the quadratic terms to zero gives us
Can you write Python code for it?

deltaOLR_linear = sympy.expand(deltaOLR).subs(delta_epsilon**2, 0)
deltaOLR_linear


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
Upwelling beam
Let $U$ be the upwelling flux of longwave radiation.
The upward flux from the surface to layer 0 is
$$ U_0 = E_s $$
(just the emission from the suface).
Can you write Python code for it?


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
We have a quadratic equation for $\epsilon$.
Now use the sympy.solve function to solve the quadratic:
Can you write Python code for it?

#  The sympy.solve method takes an expression equal to zero
#  So in this case we subtract the tuned value of OLR from our expression
eps_solution = sympy.solve(OLR2 - 238.5, epsilon)
eps_solution


I am working on the file "Lectures/Lecture07 -- Elementary greenhouse models.ipynb".
The first task is:
which then simplifies to
Can you write Python code for it?

sympy.simplify(R.subs([(T_0, T_s), (T_1, T_s)]))


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:

4. A 30-layer model using the observed temperatures
Can you write Python code for it?

#  initialize a grey radiation model with 30 levels
col = climlab.GreyRadiationModel()
print( col)

col.lev

 col.lev_bounds

# interpolate to 30 evenly spaced pressure levels
lev = col.lev
Tinterp = np.interp(lev, np.flipud(Tglobal.level), np.flipud(Tglobal))
Tinterp
#  Need to 'flipud' because the interpolation routine
#  needs the pressure data to be in increasing order

# Initialize model with observed temperatures
col.Ts[:] = Tglobal[0]
col.Tatm[:] = Tinterp

# This should look just like the observations
result_list = [col.state]
name_list = ['Observed, interpolated']
plot_soundings(result_list, name_list);


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Access these either through dictionary methods or as attributes of the model object:
Can you write Python code for it?

print( col.state['Ts'])
print( col.Ts)
col.Ts is col.state['Ts']


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
And let's now see what we got!
Can you write Python code for it?

result_list.append(oz_col.state)
name_list.append('Radiative-Convective equilibrium with O3')

#  Make a plot to compare observations, Radiative Equilibrium, Radiative-Convective Equilibrium, and RCE with ozone!
plot_soundings(result_list, name_list)


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Contents

Introducing climlab
Using climlab to implement the two-layer leaky greenhouse model
The observed annual, global mean temperature profile
A 30-layer model using the observed temperatures
Radiative forcing in the 30-layer model
Radiative equilibrium in the 30-layer model
Radiative-Convective Equilibrium in the 30-layer model
Putting stratospheric ozone in the grey-gas model

1. Introducing climlab

climlab is a flexible engine for process-oriented climate modeling.
It is based on a very general concept of a model as a collection of individual,
interacting processes. climlab defines a base class called Process, which
can contain an arbitrarily complex tree of sub-processes (each also some
sub-class of Process). Every climate process (radiative, dynamical,
physical, turbulent, convective, chemical, etc.) can be simulated as a stand-alone
process model given appropriate input, or as a sub-process of a more complex model.
New classes of model can easily be defined and run interactively by putting together an
appropriate collection of sub-processes.
climlab is a work-in-progress, and the code base will evolve substantially over the course of this semester.
The latest code can always be found on github:
https://github.com/brian-rose/climlab
You are strongly encouraged to clone the climlab repository and use git to keep your local copy up-to-date.
Running this notebook requires that climlab is already installed on your system.
Can you write Python code for it?

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
from xarray.ufuncs import cos, deg2rad, log
import climlab


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
It is now non-zero, and largest near the top of the column (also top of the array) where the ozone concentration is highest.
Now it's time to run the model out to radiative-convective equilibrium
Can you write Python code for it?

oz_col.integrate_years(1.)

print( oz_col.ASR - oz_col.OLR)


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Let's check how this changes the SW absorption:
Can you write Python code for it?


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Let's now put in the ozone:
Can you write Python code for it?

oz_col.subprocess['SW'].absorptivity = epsSW
print( oz_col.subprocess['SW'].absorptivity)


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Now we are assigning the "observed" temperatures to our model state:
Can you write Python code for it?

col.Ts[:] = 288.
col.Tatm[:] = np.array([230., 275.])
col.state

LW = col.subprocess['LW']
print( LW)

LW.absorptivity

#  copying the tuned value of epsilon from Lecture 6 notes
LW.absorptivity = 0.586
LW.absorptivity

#  This does all the calculations that would be performed at each time step,
#  but doesn't actually update the temperatures
col.compute_diagnostics()
#  Print out the dictionary
col.diagnostics

#  Check OLR against our analytical solution
col.OLR

# Like the state variables, the diagnostics can also be accessed in two different ways
col.diagnostics['OLR']

col.state

# perform a single time step
col.step_forward()

col.state


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Now we will do something new: let the column absorb some shortwave radiation. We will assume that the shortwave absorptivity is proportional to the ozone concentration we plotted above.
Now we need to weight the absorptivity by the pressure (mass) of each layer.
Can you write Python code for it?

# This number is an arbitrary parameter that scales how absorptive we are making the ozone
# in our grey gas model
ozonefactor = 75
dp = oz_col.Tatm.domain.lev.delta
epsSW = O3_global.values * dp * ozonefactor


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Some properties of the radiative equilibrium temperature profile:

The surface is warmer than observed.
The lower troposphere is colder than observed.
Very cold air is sitting immediately above the warm surface.
There is no tropopause, no stratosphere.

7. Radiative-Convective Equilibrium in the 30-layer model

We recognize that the large drop in temperature just above the surface is unphysical. Parcels of air in direct contact with the ground will be warmed by mechansisms other than radiative transfer.
These warm air parcels will then become buoyant, and will convect upward, mixing their heat content with the environment.
We parameterize the statistical effects of this mixing through a convective adjustment.
At each timestep, our model checks for any locations at which the lapse rate exceeds some threshold. Unstable layers are removed through an energy-conserving mixing formula.
This process is assumed to be fast relative to radiative heating. In the model, it is instantaneous.
Add the convective adjustment as an additional subprocess
Can you write Python code for it?

#  Here is the existing model
print( re)

#  First we make a new clone
rce = climlab.process_like(re)
#  Then create a new ConvectiveAdjustment process
conv = climlab.convection.ConvectiveAdjustment(state=rce.state,
                                               adj_lapse_rate=6.)
#  And add it to our model
rce.add_subprocess('Convective Adjustment', conv)
print( rce)


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Take the global average of the ozone climatology, and plot it as a function of pressure (or height)
Can you write Python code for it?

#  Take global, annual average and convert to Kelvin
weight_ozone = cos(deg2rad(ozone.lat)) / cos(deg2rad(ozone.lat)).mean(dim='lat')
O3_global = (ozone.O3 * weight_ozone).mean(dim=('lat','lon','time'))
print( O3_global)

ax = plt.figure(figsize=(10,8)).add_subplot(111)
ax.plot( O3_global * 1.E6, -np.log(ozone.lev/climlab.constants.ps) )
ax.set_xlabel('Ozone (ppm)', fontsize=16)
ax.set_ylabel('Pressure (hPa)', fontsize=16 )
yticks = np.array([1000., 750., 500., 250., 100., 50., 20., 10., 5.])
ax.set_yticks(-np.log(yticks/1000.))
ax.set_yticklabels(yticks)
ax.grid()
ax.set_title('Global, annual mean ozone concentration', fontsize = 24);


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
The pressure levels in this dataset are:
Can you write Python code for it?


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
Tune absorptivity to get observed OLR
Can you write Python code for it?

col.compute_diagnostics()
col.OLR

# Need to tune absorptivity to get OLR = 238.5
epsarray = np.linspace(0.01, 0.1, 100)
OLRarray = np.zeros_like(epsarray)

for i in range(epsarray.size):
    col.subprocess['LW'].absorptivity = epsarray[i]
    col.compute_diagnostics()
    OLRarray[i] = col.OLR

plt.plot(epsarray, OLRarray)
plt.grid()
plt.xlabel('epsilon')
plt.ylabel('OLR')


I am working on the file "Lectures/Lecture08 -- Grey radiation modeling with climlab.ipynb".
The first task is:
We want to use the field epsSW as the absorptivity for our SW radiation model.
Let's see what the absorptivity is current set to:
Can you write Python code for it?


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
But the model has a new input field for specific humidity:
Can you write Python code for it?


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
By default this model has convective adjustment.  We can set the adjusted lapse rate by passing a parameter when we create the model.
The model currently has no ozone (so there is no stratosphere). Not very realistic!
About the radiatively active gases
The Band model is aware of three different absorbing gases: O3 (ozone), CO2, and H2O (water vapor). The abundances of these gases are stored in a dictionary of arrays as follows:
Can you write Python code for it?


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
Check out the list of subprocesses.
We now have a process called H2O, in addition to things we've seen before.
The state variables are still just temperatures:
Can you write Python code for it?


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
Contents

What if CO$_2$ actually behaved like a Grey Gas?
Another look at observed spectra
Water vapor changes under global warming
A simple water vapor parameterization
Modeling spectral bands with the climlab.BandRCModel process

1. What if CO$_2$ actually behaved like a Grey Gas?

Suppose that CO$_2$ actually behaved as a grey gas. In other words, no spectral dependence in absorptivity.
If we then double the CO2 concentration in the atmosphere, we double the number of absorbers. This should imply that we also double the absorption cross-section:
$$ \kappa^\prime = 2 ~ \kappa $$
This would imply that we double the optical thickness of every layer:
$$ \Delta \tau^\prime  = 2 \left( -\frac{\kappa}{g} \Delta p \right) = 2 ~ \Delta \tau$$
And since (from Lecture 9) the absorptivity / emissivity of each layer is
$$  \epsilon = 1 - \exp\big( - \Delta \tau \big)  $$
the modified absorptivity is
$$ \epsilon^\prime = 1 - \exp\big( - 2\Delta \tau \big) = 1 - \left( \exp\big( - \Delta \tau \big)\right)^2 = 1 - (1-\epsilon)^2 $$
or simply
$$ \epsilon^\prime = 2 \epsilon  - \epsilon^2 $$
(Note that $\epsilon^\prime = 2 \epsilon$ for very thin layers, for which $\epsilon$ is small).
What does our 2-layer analytical model then say about the radiative forcing?
Recall that we tuned the two-layer model with
$$ \epsilon = 0.586 $$
to get the observed OLR with observed temperatures.
Can you write Python code for it?

#  Applying the above formula
eps = 0.586
print( 'Doubling a grey gas absorber would \
       change the absorptivity from {:.3} \
       to {:.3}'.format(eps, 2*eps - eps**2))


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
First try a model with all default parameters. Usage is very similar to the familiar RadiativeConvectiveModel.
Can you write Python code for it?

col1 = climlab.BandRCModel()
print( col1)


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
Interesting that the model is MORE sensitive when ozone is set to zero.
<div class="alert alert-success">
[Back to ATM 623 notebook home](../index.ipynb)
</div>

Version information
Can you write Python code for it?

%load_ext version_information
%version_information numpy, matplotlib, xarray, climlab


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
Ozone and CO2 are both specified in the model. The default, as you see above, is zero ozone, and constant (well-mixed) CO2 at a volume mixing ratio of 3.8E-4 or 380 ppm.
Water vapor is handled differently: it is determined by the model at each timestep. We make the following assumptions, following a classic paper on radiative-convective equilibrium by Manabe and Wetherald (J. Atmos. Sci. 1967):

the relative humidity just above the surface is fixed at 77% (can be changed of course... see the parameter col1.relative_humidity
water vapor drops off linearly with pressure
there is a small specified amount of water vapor in the stratosphere.

Putting in some ozone
We need to provide some ozone data to the model in order to simulate a stratosphere. We will read in some ozone data just as we did in Lecture 8.
Can you write Python code for it?

ozone = xr.open_dataset( datapath + 'som_input/ozone_1.9x2.5_L26_2000clim_c091112.nc')

#  Take global, annual average and convert to Kelvin
weight_ozone = cos(deg2rad(ozone.lat)) / cos(deg2rad(ozone.lat)).mean(dim='lat')
O3_global = (ozone.O3 * weight_ozone).mean(dim=('lat','lon','time'))
print(O3_global)

fig, ax = plt.subplots()
ax.plot( O3_global*1E6, ozone.lev)
ax.invert_yaxis()
ax.set_xlabel('Ozone (ppm)', fontsize=16)
ax.set_ylabel('Pressure (hPa)', fontsize=16 )
ax.set_title('Global, annual mean ozone concentration', fontsize = 16);
fig


I am working on the file "Lectures/Lecture10 -- Who needs spectral bands.ipynb".
The first task is:
We are going to create another instance of the model, this time using the same vertical coordinates as the ozone data.
Can you write Python code for it?

#  Create the column with appropriate vertical coordinate, surface albedo and convective adjustment
col2 = climlab.BandRCModel(lev=ozone.lev)
print( col2)

#  Set the ozone mixing ratio
col2.absorber_vmr['O3'] = O3_global.values

#  Run the model out to equilibrium!
col2.integrate_years(2.)

fig, ax = plt.subplots()
ax.plot( col1.Tatm, np.log(col1.lev/1000), 'c-', label='RCE' )
ax.plot( col1.Ts, 0, 'co', markersize=16 )
ax.plot(col2.Tatm, np.log(col2.lev/1000), 'r-', label='RCE O3' )
ax.plot(col2.Ts, 0, 'ro', markersize=16 )
ax.invert_yaxis()
ax.set_xlabel('Temperature (K)', fontsize=16)
ax.set_ylabel('log(Pressure)', fontsize=16 )
ax.set_title('Temperature profiles', fontsize = 18)
ax.grid(); ax.legend()
fig


I am working on the file "Lectures/Lecture12 -- CESM climate sensitivity.ipynb".
The first task is:
Credits
The author of this notebook is Brian E. J. Rose, University at Albany.
It was developed in support of ATM 623: Climate Modeling, a graduate-level course in the Department of Atmospheric and Envionmental Sciences
Development of these notes and the climlab software is partially supported by the National Science Foundation under award AGS-1455071 to Brian Rose. Any opinions, findings, conclusions or recommendations expressed here are mine and do not necessarily reflect the views of the National Science Foundation.

Appendix: for later reference, here is how you can open the other output types
The following will open the rest of the CESM output (land, sea ice, river routing, ocean).
These are not needed for the above homework assignment, but may be useful later on.
Can you write Python code for it?

# # make a dictionary of all the CLM land model output
# land = {}
# for name in casenames:
#     path = casepaths[name] + casenames[name] + '.clm2.h0.nc'
#     print('Attempting to open the dataset ', path)
#     land[name] = xr.open_dataset(path)

# # make a dictionary of all the sea ice model output
# ice = {}
# for name in casenames:
#     path = casepaths[name] + casenames[name] + '.cice.h.nc'
#     print('Attempting to open the dataset ', path)
#     ice[name] = xr.open_dataset(path)

# # make a dictionary of all the river transport output
# rtm = {}
# for name in casenames:
#     path = casepaths[name] + casenames[name] + '.rtm.h0.nc'
#     print('Attempting to open the dataset ', path)
#     rtm[name] = xr.open_dataset(path)

# ocn = {}
# for name in casenames:
#     if 'cpl' in name:
#         path = casepaths[name] + casenames[name] + '.pop.h.nc'
#         print('Attempting to open the dataset ', path)
#         ocn[name] = xr.open_dataset(path)


I am working on the file "Lectures/Lecture12 -- CESM climate sensitivity.ipynb".
The first task is:
Issues to think about here include:

Why is the annual average here only approximate? (think about the calendar)
Why is there an annual cycle in the global average temperature? (planet is coldest during NH winter)
Different character of the temperature variability in the coupled vs. slab model
Much more rapid warming in the Slab Ocean Model

Now we can work on computing ECS and TCR
Can you write Python code for it?

# extract the last 10 years from the slab ocean control simulation
# and the last 20 years from the coupled control
nyears_slab = 10
nyears_cpl = 20
clim_slice_slab = slice(-(nyears_slab*12),None)
clim_slice_cpl = slice(-(nyears_cpl*12),None)

# extract the last 10 years from the slab ocean control simulation
T0_slab = TREFHT_global['som_control'].isel(time=clim_slice_slab).mean(dim='time')
T0_slab

# and the last 20 years from the coupled control
T0_cpl = TREFHT_global['cpl_control'].isel(time=clim_slice_cpl).mean(dim='time')
T0_cpl

# extract the last 10 years from the slab 2xCO2 simulation
T2x_slab = TREFHT_global['som_2xCO2'].isel(time=clim_slice_slab).mean(dim='time')
T2x_slab

# extract the last 20 years from the coupled CO2 ramp simulation
T2x_cpl = TREFHT_global['cpl_CO2ramp'].isel(time=clim_slice_cpl).mean(dim='time')
T2x_cpl

ECS = T2x_slab - T0_slab
TCR = T2x_cpl - T0_cpl
print('The Equilibrium Climate Sensitivity is {:.3} K.'.format(float(ECS)))
print('The Transient Climate Response is {:.3} K.'.format(float(TCR)))


I am working on the file "Lectures/Lecture12 -- CESM climate sensitivity.ipynb".
The first task is:
Issues to think about:

Why do we talk about fractional changes in CO2, such as "doubling atmospheric CO2", and "1%/year compounded CO2 increase?
Why not instead talk about changes in absolute amounts of CO2?

The answer is closely related to the fact that the radiative forcing associated with CO2 increase is approximately logarithmic in CO2 amount. So a doubling of CO2 represents roughly the same radiative forcing regardless of the initial CO2 concentration.
Compute and plot time series of global, annual mean near-surface air temperature in all four simulations
Can you write Python code for it?

# The surface air temperature, which we will use for our sensitivity metrics
atm['cpl_control'].TREFHT

#  The area weighting needed for global averaging
gw = atm['som_control'].gw
print(gw)

def global_mean(field, weight=gw):
    '''Return the area-weighted global average of the input field'''
    return (field*weight).mean(dim=('lat','lon'))/weight.mean(dim='lat')

#  Loop through the four simulations and produce the global mean timeseries
TREFHT_global = {}
for name in casenames:
    TREFHT_global[name] = global_mean(atm[name].TREFHT)


I am working on the file "Lectures/Lecture12 -- CESM climate sensitivity.ipynb".
The first task is:
Make maps of the surface air temperature anomaly due to CO2 doubling in both the slab and coupled models
Can you write Python code for it?

Tmap_cpl_2x = atm['cpl_CO2ramp'].TREFHT.isel(time=clim_slice_cpl).mean(dim='time')
Tmap_cpl_control = atm['cpl_control'].TREFHT.isel(time=clim_slice_cpl).mean(dim='time')
DeltaT_cpl = Tmap_cpl_2x - Tmap_cpl_control

Tmap_som_2x = atm['som_2xCO2'].TREFHT.isel(time=clim_slice_slab).mean(dim='time')
Tmap_som_control = atm['som_control'].TREFHT.isel(time=clim_slice_slab).mean(dim='time')
DeltaT_som = Tmap_som_2x - Tmap_som_control

fig, axes, cx = make_map(DeltaT_cpl)
fig.suptitle('Surface air temperature anomaly (coupled transient)', fontsize=16);
axes[1].set_xlim(0,7)  # ensure the line plots have same axes
cx.set_clim([0, 8])    # ensure the contour maps have the same color intervals

fig, axes,cx  = make_map(DeltaT_som)
fig.suptitle('Surface air temperature anomaly (equilibrium SOM)', fontsize=16);
axes[1].set_xlim(0,7)
cx.set_clim([0, 8])


I am working on the file "Lectures/Lecture12 -- CESM climate sensitivity.ipynb".
The first task is:
Make some pretty timeseries plots, including an approximate running annual average
Can you write Python code for it?

fig, axes = plt.subplots(2,1,figsize=(10,8))
for name in casenames:
    if 'cpl' in name:
        ax = axes[0]
        ax.set_title('Fully coupled ocean')
    else:
        ax = axes[1]
        ax.set_title('Slab ocean')
    field = TREFHT_global[name]
    field_running = field.rolling(time=12, center=True).mean()
    line = ax.plot(field.time / days_per_year,
                   field,
                   label=name,
                   linewidth=0.75,
                   )
    ax.plot(field_running.time / days_per_year,
            field_running,
            color=line[0].get_color(),
            linewidth=2,
           )
for ax in axes:
    ax.legend();
    ax.set_xlabel('Years')
    ax.set_ylabel('Temperature (K)')
    ax.grid();
    ax.set_xlim(0,100)
fig.suptitle('Global mean surface air temperature in CESM simulations', fontsize=16);


I am working on the file "Lectures/Lecture12 -- CESM climate sensitivity.ipynb".
The first task is:
TCR is always smaller than ECS due to the transient effects of ocean heat uptake.
We are going to estimate the ECS of the fully coupled model by using the equilibrium response of the Slab Ocean .
Can you write Python code for it?

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
An idealized transient global warming scenario: CO2 increases by 1%/year to doubling.
Now consider the CO2 increase. In the real world, CO2 has been increasing every year since the beginning of industrialization. Future CO2 concentrations depend on collective choices made by human societies about how much fossil fuel to extract and burn.
We will set up a simple scenario. Suppose that CO2 increases by 1% of its existing concentration every year until it reaches 2x its initial concentration. This takes about 70 years.
After 70 years, we assume that all anthropogenic emissions, and CO2 concentration is stabilized at the 2x level.
What happens to the surface temperature?
How do the histories of surface and deep ocean temperature compare in our two models?
We are going to simulation 400 years of transient global warming in the two models.
<div class="alert alert-success">
This code will take a long time to run! While it's running, we'll think about what the result might look like
</div>
Can you write Python code for it?

num_years = 400
years = np.arange(num_years+1)

Tsarray = []
Tocean = []
netrad = []
for n in range(len(deep)):
    thisTs = np.nan * np.zeros(num_years+1)
    thisnetrad = np.nan * np.zeros(num_years+1)
    thisTocean = np.nan * np.zeros((deep[n].Tocean.size, num_years+1))
    thisTs[0] = deep[n].Ts
    thisnetrad[0] = deep[n].ASR - deep[n].OLR
    thisTocean[:, 0] = deep[n].Tocean
    Tsarray.append(thisTs)
    Tocean.append(thisTocean)
    netrad.append(thisnetrad)

CO2initial = deep[0].subprocess['Radiation'].absorber_vmr['CO2']
CO2array = np.nan * np.zeros(num_years+1)
CO2array[0] = CO2initial * 1E6

#  Increase CO2 by 1% / year for 70 years (until doubled), and then hold constant
for y in range(num_years):
    if deep[0].subprocess['Radiation'].absorber_vmr['CO2'] < 2 * CO2initial:
        for model in deep:
            model.subprocess['Radiation'].absorber_vmr['CO2'] *= 1.01
    CO2array[y+1] = deep[0].subprocess['Radiation'].absorber_vmr['CO2'] * 1E6
    print('Year ', y+1, ', CO2 mixing ratio is ', CO2array[y+1],' ppm.')

    for n, model in enumerate(deep):
        for m in range(steps_per_year):
            qsat = climlab.utils.thermo.qsat(model.Tatm, model.lev)
            model.subprocess['Radiation'].specific_humidity[:] = rh * qsat
            DeltaTs = model.Ts - slab_control[n].Ts
            model.subprocess['Convection'].adj_lapse_rate = 6.5 + lapse_change_factor[n]*DeltaTs
            model.step_forward()

            Tsarray[n][y+1] = model.Ts
            Tocean[n][:, y+1] = model.Tocean
            netrad[n][y+1] = model.ASR - model.OLR

colorlist = ['b', 'r']
co2color = 'k'

num_axes = len(deep) + 1
fig, ax = plt.subplots(num_axes, figsize=(12,14))

# Twin the x-axis twice to make independent y-axes.
topaxes = [ax[0], ax[0].twinx(), ax[0].twinx()]

# Make some space on the right side for the extra y-axis.
fig.subplots_adjust(right=0.85)

# Move the last y-axis spine over to the right by 10% of the width of the...


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
Integrate the control model out to equilibrium.
Can you write Python code for it?

rcm_control.integrate_years(5)
rcm_control.ASR - rcm_control.OLR


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
Now here is where our two models will differ:
We are going to assign them different lapse rate feedbacks.
Similar to the exercise in Assignment 3, we are going to assume
$$ \Gamma = \Gamma_{ref} + \gamma * \Delta T_s $$
where $\Gamma_{ref} = 6.5 K/km$ is the critical lapse rate in our control climate, and $\gamma$ is a number in units of km$^{-1}$ that determines how much the critical lapse rate should change per degree warming.
We are going to investigate two different assumptions:

temperatures decrease more with height under global warming, $\gamma = +0.3$ km$^{-1}$
temperature decrease less with height under global warming, $\gamma = -0.3$ km$^{-1}$
Can you write Python code for it?

lapse_change_factor = [+0.3, -0.3]

for n in range(len(slab_2x)):
    rcm_2xCO2 = slab_2x[n]
    print('Integrating ' + rcm_2xCO2.name)
    for m in range(5 * steps_per_year):
        # At every timestep
        # we calculate the new saturation specific humidity for the new temperature
        #  and change the water vapor in the radiation model
        #  so that relative humidity is always the same
        qsat = climlab.utils.thermo.qsat(rcm_2xCO2.Tatm, rcm_2xCO2.lev)
        rcm_2xCO2.subprocess['Radiation'].specific_humidity[:] = rh * qsat
        #  We also adjust the critical lapse rate in our convection model
        DeltaTs = rcm_2xCO2.Ts - rcm_control.Ts
        rcm_2xCO2.subprocess['Convection'].adj_lapse_rate = 6.5 + lapse_change_factor[n]*DeltaTs
        rcm_2xCO2.step_forward()
    print('The TOA imbalance is %0.5f W/m2' %(rcm_2xCO2.ASR-rcm_2xCO2.OLR))
    print('The ECS is %0.3f K' %(rcm_2xCO2.Ts - rcm_control.Ts))
    print('')


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
Now let's make two copies of this model and keep them in a list:
Can you write Python code for it?

slab_control = []
slab_control.append(rcm_control)
slab_control.append(climlab.process_like(rcm_control))


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
The "ocean" in these models is just a "slab" of water 2.5 meter deep.
That's all we need to calculate the equilibrium temperatures, but it tells us nothing about the timescales for climate change in the real world.
For this, we need a deep ocean that can exchange heat with the surface.
Transient warming scenarios in column models with ocean heat uptake
We are now going to build two new models. The atmosphere (radiative-convective model) will be identical to the two "slab" models we just used. But these will be coupled to a column of ocean water 2000 m deep!
We will parameterize the ocean heat uptake as a diffusive mixing process. Much like when we discussed the diffusive parameterization for atmospheric heat transport -- we are assuming that ocean dynamics result in a vertical mixing of heat from warm to cold temperatures.
The following code will set this up for us.
We will make one more assumption, just for the sake of illustration:
The more sensitive model (Model 0) is also more efficent at taking up heat into the deep ocean
Can you write Python code for it?

#  Create the domains
ocean_bounds = np.arange(0., 2010., 100.)
depthax = climlab.Axis(axis_type='depth', bounds=ocean_bounds)
ocean = climlab.domain.domain.Ocean(axes=depthax)
atm = slab_control[0].Tatm.domain

#  Model 0 has a higher ocean heat diffusion coefficient --
#  a more efficent deep ocean heat sink
ocean_diff = [5.E-4, 3.5E-4]

#  List of deep ocean models
deep = []
for n in range(len(slab_control)):
    rcm_control = slab_control[n]
    #  Create the state variables
    Tinitial_ocean = rcm_control.Ts * np.ones(ocean.shape)
    Tocean = climlab.Field(Tinitial_ocean.copy(), domain=ocean)
    Tatm = climlab.Field(rcm_control.Tatm.copy(), domain=atm)

    #  Surface temperature Ts is the upper-most grid box of the ocean
    Ts = Tocean[0:1]
    atm_state = {'Tatm': Tatm, 'Ts': Ts}

    rad = climlab.radiation.RRTMG(name='Radiation',
                                  state=atm_state,
                                  specific_humidity=Qglobal.values,
                                  timestep = deltat,
                                  albedo = 0.25,
                                 )
    conv = climlab.convection.ConvectiveAdjustment(name='Convection',
                                                   state=atm_state,
                                                   adj_lapse_rate=6.5,
                                                   timestep=rad.timestep,)

    model = rad + conv
    if n == 0:
        model.name = 'RCM with high sensitivity and efficient heat uptake'
    elif n == 1:
        model.name = 'RCM with low sensitivity and inefficient heat uptake'
    model.set_state('Tocean', Tocean)
    diff = climlab.dynamics.Diffusion(state={'Tocean': model.Tocean},
                                K=ocean_diff[n],
                                diffusion_axis='depth',
                                timestep=deltat * 10,)
    model.add_subprocess('Ocean Heat Uptake', diff)
    print('')
    print(model)
    print('')
    deep.append(model)


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
Transient vs. equilibrium warming: key points

During the first 70 years, the radiative forcing goes up every year
The warming in the two models is almost identical during this phase
After year 70, the CO2 levels are stable and so the radiative forcing is no longer increasing
Both models continue to warm for hundreds of years
The difference between the two models become larger over time
In either case, at the time of CO2 doubling the model has achieved only a fraction of its equilibrium surface warming.
The difference between the warming at year 70 and the equilibrium warming is called the committed warming. It is the global warming associated with CO2 emissions that are already in the atmosphere.
How do we know at year 70 what the committed warming is?  Are we on the blue or the red path? At year 70, have we achieved half or only a third of the eventual equilibrium warming?
In our example, the more sensitive model also has more efficient ocean heat uptake, so the initial warming looks identical.
Uncertainties in both climate feedback processes and ocean heat uptake processes contribute to uncertainty in the rate of global warming

Results from comprehensive coupled GCMs
Fast and slow components of the warming
This figure shows how a comprehensive coupled GCM responds to the same kind of idealized CO2 increase we have looked at above: CO2 increases at 1%/year for 70 years and is then held constant at twice the pre-industrial level (blue curve), or at 4x the pre-industrial level (red curve).
The dashed curve show the ocean heat content continuing to rise slowly over thousands of years.
<img src="http://www.atmos.albany.edu/facstaff/brose/classes/ENV415_Spring2018/images/Yoshimori_transientwarming.png" width="400">

M. Yoshimori, M. Watanabe, H. Shiogama, A. Oka, A. Abe-Ouchi, R. Ohgaito, and Y. Kamae. A review of progress towards understanding the transient global mean surface temperature response to radiative perturbation. Prog. Earth Planet. Sic., 3, 2016.

What happens if CO2 levels are abruptly returned...
Can you write Python code for it?


I am working on the file "Lectures/Lecture13 -- TransientWarming.ipynb".
The first task is:
We are going to double CO2 in both models and label them as high and low sensitivity. We will build in different feedbacks into our two columns.
Can you write Python code for it?

slab_2x = []
for n in range(len(slab_control)):
    rcm_2xCO2 = climlab.process_like(rcm_control)
    rcm_2xCO2.subprocess['Radiation'].absorber_vmr['CO2'] *= 2.
    if n == 0:
        rcm_2xCO2.name = 'High-sensitivity RCM'
    elif n == 1:
        rcm_2xCO2.name = 'Low-sensitivity RCM'
    slab_2x.append(rcm_2xCO2)


I am working on the file "Lectures/Lecture15 -- Insolation.ipynb".
The first task is:
Also plot the zonally averaged insolation at a few different times of the year:
Can you write Python code for it?

summer_solstice = 170
winter_solstice = 353
fig, ax = plt.subplots(figsize=(10,8))
ax.plot( lat, Q[:,(summer_solstice, winter_solstice)] );
ax.plot( lat, np.mean(Q, axis=1), linewidth=2 )
ax.set_xbound(-90, 90)
ax.set_xticks( range(-90,100,30) )
ax.set_xlabel('Latitude', fontsize=16 );
ax.set_ylabel('Insolation (W m$^{-2}$)', fontsize=16 );
ax.grid()


I am working on the file "Lectures/Lecture15 -- Insolation.ipynb".
The first task is:
And make a contour plot of Q as function of latitude and time of year.
Can you write Python code for it?

fig, ax = plt.subplots(figsize=(10,8))
CS = ax.contour( days, lat, Q , levels = np.arange(0., 600., 50.) )
ax.clabel(CS, CS.levels, inline=True, fmt='%r', fontsize=10)
ax.set_xlabel('Days since January 1', fontsize=16 )
ax.set_ylabel('Latitude', fontsize=16 )
ax.set_title('Daily average insolation', fontsize=24 )
ax.contourf ( days, lat, Q, levels=[-1000., 0.], colors='k' )


I am working on the file "Lectures/Lecture15 -- Insolation.ipynb".
The first task is:
Contents

Distribution of insolation
Computing daily insolation with climlab
Global, seasonal distribution of insolation (present-day orbital parameters)

1. Distribution of insolation

These notes closely follow section 2.7 of Dennis L. Hartmann, "Global Physical Climatology", Academic Press 1994.
The amount of solar radiation incident on the top of the atmosphere (what we call the "insolation") depends on

latitude
season
time of day

This insolation is the primary driver of the climate system. Here we will examine the geometric factors that determine insolation, focussing primarily on the daily average values.
Solar zenith angle
We define the solar zenith angle $\theta_s$ as the angle between the local normal to Earth's surface and a line between a point on Earth's surface and the sun.

From the above figure (reproduced from Hartmann's book), the ratio of the shadow area to the surface area is equal to the cosine of the solar zenith angle.
Instantaneous solar flux
We can write the solar flux per unit surface area as
$$ Q = S_0 \left( \frac{\overline{d}}{d} \right)^2 \cos \theta_s $$
where $\overline{d}$ is the mean distance for which the flux density $S_0$ (i.e. the solar constant) is measured, and $d$ is the actual distance from the sun.
Question:

what factors determine $\left( \frac{\overline{d}}{d} \right)^2$ ?
under what circumstances would this ratio always equal 1?

Calculating the zenith angle
Just like the flux itself, the solar zenith angle depends latitude, season, and time of day.
Declination angle
The seasonal dependence can be expressed in terms of the declination angle of the sun: the latitude of the point on the surface of Earth directly under the sun at noon (denoted by $\delta$).
$\delta$ currenly varies between +23.45º at northern summer solstice (June 21) to -23.45º at northern winter solstice (Dec. 21).
Hour angle
The hour angle $h$ is defined as the longitude of the subsolar point relative to its position at noon.
Formula...
Can you write Python code for it?

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from climlab import constants as const
from climlab.solar.insolation import daily_insolation


I am working on the file "Lectures/Lecture15 -- Insolation.ipynb".
The first task is:
First, get a little help on using the daily_insolation function:
Can you write Python code for it?


I am working on the file "Lectures/Lecture15 -- Insolation.ipynb".
The first task is:
Time and space averages
Take the area-weighted global, annual average of Q...
Can you write Python code for it?

Qaverage = np.average(np.mean(Q, axis=1), weights=np.cos(np.deg2rad(lat)))
print( 'The annual, global average insolation is %.2f W/m2.' %Qaverage)


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
Compare this with the same calculation for default (present-day) orbital parameters:
Can you write Python code for it?


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
Indeed, there was an increase of 60 W m$^{-2}$ over a 10 kyr interval following the LGM.
Why?
What orbital factors favor high insolation at 65ºN at summer solstice?

high obliquity
large, positive precessional parameter

Looking back at our plots of the orbital parameters, it turns out that both were optimal around 10,000 years ago.
Actually 10,000 years ago the climate was slightly warmer than today and the ice sheets had mostly disappeared already.
The LGM occurred near a minimum in summer insolation in the north – mostly due to obliquity reaching a minimum, since we have been in a period of weak precession due to the nearly circular orbit. So this is consistent with the orbital theory.
The hypothesis is incomplete, but compelling.
Comparing insolation at 10 kyr and 23 kyr
Can you write Python code for it?

lat = np.linspace(-90, 90, 181)
days = np.linspace(1.,50.)/50 * const.days_per_year

orb_0 = OrbitalTable.interp(kyear=0)  # present-day orbital parameters
orb_10 = OrbitalTable.interp(kyear=-10)  # orbital parameters for 10 kyrs before present
orb_23 = OrbitalTable.interp(kyear=-23)   # 23 kyrs before present
Q_0 = daily_insolation( lat, days, orb_0 )
Q_10 = daily_insolation( lat, days, orb_10 )   # insolation arrays for each of the three sets of orbital parameters
Q_23 = daily_insolation( lat, days, orb_23 )

fig = plt.figure( figsize=(12,6) )

ax1 = fig.add_subplot(1,2,1)
Qdiff = Q_10 - Q_23
CS1 = ax1.contour( days, lat, Qdiff, levels = np.arange(-100., 100., 10.) )
ax1.clabel(CS1, CS1.levels, inline=True, fmt='%r', fontsize=10)
ax1.contour( days, lat, Qdiff, levels = [0], colors='k' )
ax1.set_xlabel('Days since January 1', fontsize=16 )
ax1.set_ylabel('Latitude', fontsize=16 )
ax1.set_title('Insolation differences: 10 kyrs - 23 kyrs', fontsize=18 )

ax2 = fig.add_subplot(1,2,2)
ax2.plot( np.mean( Qdiff, axis=1 ), lat )
ax2.set_xlabel('W m$^{-2}$', fontsize=16 )
ax2.set_ylabel( 'Latitude', fontsize=16 )
ax2.set_title(' Annual mean differences', fontsize=18 )
ax2.set_ylim((-90,90))
ax2.grid()


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
The difference is tiny (and due to very small changes in the eccentricity).
Ice ages are driven by seasonal and latitudinal redistributions of solar energy, NOT by changes in the total global amount of solar energy!

7. Understanding the effects of orbital variations on insolation

We are going to create a figure showing past time variations in three quantities:

Global, annual mean insolation
Annual mean insolation at high northern latitudes
Summer solstice insolation at high northern latitudes

which we will compare to the orbital variations we plotted earlier.
Create a large array of insolation over the whole globe, whole year, and for every set of orbital parameters.
Can you write Python code for it?

lat = np.linspace(-90, 90, 91)
num = 365.
days = np.linspace(1.,num,365)/num * const.days_per_year
Q = daily_insolation(lat, days, orb)
print( Q.shape)

Qann = np.mean(Q, axis=1)  # time average over the year
print( Qann.shape)
Qglobal = np.empty_like( kyears )
for n in range( kyears.size ):   # global area-weighted average
    Qglobal[n] = np.average( Qann[:,n], weights=np.cos(np.deg2rad(lat)))
print( Qglobal.shape)

fig = plt.figure(figsize = (16,10))
ax = []
for n in range(6):
    ax.append(fig.add_subplot(3,2,n+1))

ax[0].plot( kyears, orb['ecc'] )
ax[0].set_title('Eccentricity $e$', fontsize=18 )
ax[2].plot( kyears, orb['obliquity'] )
ax[2].set_title('Obliquity (axial tilt) $\Phi$', fontsize=18 )
ax[4].plot( kyears, orb['ecc'] * np.sin( np.deg2rad( orb['long_peri'] ) ) )
ax[4].set_title('Precessional parameter $e \sin(\Lambda)$', fontsize=18 )

ax[1].plot( kyears, Qglobal )
ax[1].set_title('Global, annual mean insolation', fontsize=18 )
ax[1].ticklabel_format( useOffset=False )

ax[3].plot( kyears, Qann[80,:] )
ax[3].set_title('Annual mean insolation at 70N', fontsize=18 )

ax[5].plot( kyears, Q[80,170,:] )
ax[5].set_title('Summer solstice insolation at 70N', fontsize=18 )

for n in range(6):
    ax[n].grid()
for n in [4,5]:
    ax[n].set_xlabel( 'Thousands of years before present', fontsize=14 )


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
The object called
orb
is an xarray.Dataset which now holds 1 million years worth of orbital data, total of 1001 data points for each element:

eccentricity ecc
obliquity angle obliquity
solar longitude of perihelion long_peri
Can you write Python code for it?

fig = plt.figure( figsize = (8,8) )
ax1 = fig.add_subplot(3,1,1)
ax1.plot( kyears, orb['ecc'] )
ax1.set_title('Eccentricity $e$', fontsize=18 )
ax2 = fig.add_subplot(3,1,3)
ax2.plot( kyears, orb['ecc'] * np.sin( np.deg2rad( orb['long_peri'] ) ) )
ax2.set_title('Precessional parameter $e \sin(\Lambda)$', fontsize=18 )
ax2.set_xlabel( 'Thousands of years before present', fontsize=14 )
ax3 = fig.add_subplot(3,1,2)
ax3.plot( kyears, orb['obliquity'] )
ax3.set_title('Obliquity (axial tilt) $\Phi$', fontsize=18 )


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
This figure shows that the insolation at summer solstice does not tell the whole story!
For example, the insolation in late summer / early fall apparently got weaker between 23 and 10 kyr (in the high northern latitudes).
The annual mean plot is perfectly symmetric about the equator.
This actually shows a classic obliquity signal: at 10 kyrs, the axis close to its maximum tilt, around 24.2º. At 23 kyrs, the tilt was much weaker, only about 22.7º. In the annual mean, a stronger tilt means more sunlight to the poles and less to the equator. This is very helpful if you are trying to melt an ice sheet.
Finally, take the global average of the difference:
Can you write Python code for it?

print( np.average(np.mean(Qdiff,axis=1), weights=np.cos(np.deg2rad(lat))) )


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
Timescales of orbital variation:

Eccentricity varies slowly between nearly circular and slightly eccentric, with dominant periodicities of about 100 and 400 kyear. Current eccentricity is relatively small compared to previous few million years
Longitude of perihelion has a period around 20 kyears, but effect is modulated by slow eccentricity variations. Precessional cycles are predicted to be small for the coming 50 kyears because of weak eccentricity.
Obliquity varies between about 22.5º and 24.5º over a period of 40 kyears. It is currently near the middle of its range.

5. Using climlab to calculate insolation for arbitrary orbital parameters

We can use the function
climlab.solar.insolation.daily_insolation()
to calculate insolation for any arbitrary orbital parameters.
We just need to pass a dictionary of orbital parameters. This works automatically when we slice or interpolate from the xarray object OrbitalTable.
An example: zero obliquity
Calculate the insolation at the North Pole for a planet with zero obliquity and zero eccentricity.
Can you write Python code for it?

from climlab.solar.insolation import daily_insolation

thisorb = {'ecc':0., 'obliquity':0., 'long_peri':0.}

days = np.linspace(1.,20.)/20 * const.days_per_year
daily_insolation(90, days, thisorb)


I am working on the file "Lectures/Lecture16 -- Orbital variations.ipynb".
The first task is:
We see that

Global annual mean insolation varies only with eccentricity (slow), and the variations are very small!
Annual mean insolation varies with obliquity (medium). Annual mean insolation does NOT depend on precession!
Summer solstice insolation at high northern latitudes is affected by both precession and obliquity. The variations are large.

8. Summary

The annual, global mean insolation varies only as a result of eccentricity $e$. The changes are very small (about 0.1 % through a typical eccentricity cycle from more circular to more elliptical)
Obliquity controls the annual-mean equator-to-pole insolation gradient.

The precessional parameter $e \sin\Lambda$ controls the modulation in seasonal insolation due to eccentricity and longitude of perihelion $\Lambda$.

The combined effects can result in 15% changes in high-latitude summer insolation

Obliquity combined with eccentricity and longitude of perihelion control the amplitude of seasonal insolation variations at a point.
Combined effects of the three orbital parameters can cause variations in seasonal insolation as large as 30% in high latitudes.

These geometrical considerations tell us that seasonal variations in $Q$ can be rather large, and will surely impact the climate. But to go from there to understanding how large ice sheets come and go is a difficult step, and requires climate models!
One thing is clear: any serious astronomical theory of climate needs to take account of the climate response to seasonal variations in Q, because these are much larger than the variations in annual mean insolation.

[Back to ATM 623 notebook home](../index.ipynb)

Version information
Can you write Python code for it?

%load_ext version_information
%version_information numpy, matplotlib, climlab


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
All climlab Process objects have an attribute called timeave.
This is a dictionary of time-averaged diagnostics, which are automatically calculated during the most recent call to integrate_years() or integrate_days().
Can you write Python code for it?


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Compare annual average temperature in RCE to the zonal-, annual mean observations.
Can you write Python code for it?

# Plot annual mean surface temperature in the model,
#   compare to observed annual mean surface temperatures
fig, ax = plt.subplots()
ax.plot(model.lat, model.timeave['Ts'], label='RCE')
ax.plot(ncep_temp_zon.lat, ncep_temp_zon.mean(dim='time'), label='obs')
ax.set_xticks(range(-90,100,30))
ax.grid(); ax.legend();


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Contents

Spatial patterns of insolation and surface temperature
Calculating Radiative-Convective Equilibrium as a function of latitude
Observed and modeled TOA radiation budget
The energy budget for a zonal band
Observed and modeled poleward heat transport
Energy budgets for the atmosphere and ocean
Calculating the partitioning of poleward energy transport into different components
Mechanisms of heat transport

1. Spatial patterns of insolation and surface temperature

Let's take a look at seasonal and spatial pattern of insolation and compare this to the zonal average surface temperatures.
Can you write Python code for it?

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
import climlab
from climlab import constants as const

#  Calculate daily average insolation as function of latitude and time of year
lat = np.linspace( -90., 90., 500 )
days = np.linspace(0, const.days_per_year, 365 )
Q = climlab.solar.insolation.daily_insolation( lat, days )

##  daily surface temperature from  NCEP reanalysis
ncep_url = "http://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/ncep.reanalysis.derived/"
ncep_temp = xr.open_dataset( ncep_url + "surface_gauss/skt.sfc.day.1981-2010.ltm.nc", decode_times=False)
#url = 'http://apdrc.soest.hawaii.edu:80/dods/public_data/Reanalysis_Data/NCEP/NCEP/clima/'
#skt_path = 'surface_gauss/skt'
#ncep_temp = xr.open_dataset(url+skt_path)
ncep_temp_zon = ncep_temp.skt.mean(dim='lon')

fig = plt.figure(figsize=(12,6))

ax1 = fig.add_subplot(121)
CS = ax1.contour( days, lat, Q , levels = np.arange(0., 600., 50.) )
ax1.clabel(CS, CS.levels, inline=True, fmt='%r', fontsize=10)
ax1.set_title('Daily average insolation', fontsize=18 )
ax1.contourf ( days, lat, Q, levels=[-100., 0.], colors='k' )

ax2 = fig.add_subplot(122)
CS = ax2.contour( (ncep_temp.time - ncep_temp.time[0])/const.hours_per_day, ncep_temp.lat,
                 ncep_temp_zon.T, levels=np.arange(210., 310., 10. ) )
ax2.clabel(CS, CS.levels, inline=True, fmt='%r', fontsize=10)
ax2.set_title('Observed zonal average surface temperature', fontsize=18 )

for ax in [ax1,ax2]:
    ax.set_xlabel('Days since January 1', fontsize=16 )
    ax.set_ylabel('Latitude', fontsize=16 )
    ax.set_yticks([-90,-60,-30,0,30,60,90])
    ax.grid()


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Discuss the shape of these curves.
Actually very very similar before and after the global warming.

8. Mechanisms of heat transport

Energy is transported across latitude lines whenever there is an exchange of fluids with different energy content: e.g. warm fluid moving northward while colder fluid moves southward.
Thus energy transport always involves correlations between northward component of velocity $v$ and energy $e$
The transport is an integral of these correlations, around a latitude circle and over the depth of the fluid:
$$ \mathcal{H} = \int_0^{2\pi} \int_{\text{bottom}}^{\text{top}} \rho ~ v ~ e ~ dz ~ a \cos\phi ~ d\lambda$$
The total transport (which we have been inferring from the TOA radiation imbalance) includes contributions from both the atmosphere and the ocean:
$$ \mathcal{H} = \mathcal{H}{a} + \mathcal{H}{o} $$
We can apply the above definition to both fluids (with appropriate values for bottom and top in the depth integral).
The appropriate measure of energy content is different for the atmosphere and ocean.
For the ocean, we usually use the enthalpy for an incompressible fluid:
$$ e_o \approx c_w ~ T $$
where $c_w \approx 4.2 \times 10^{3}$ J kg$^{-1}$ K$^{-1}$ is the specific heat for seawater.
For the atmosphere, it's a bit more complicated. We need to account for both the compressibility of air, and for its water vapor content. This is because of the latent energy associated with evaporation and condensation of vapor.
It is convenient to define the moist static energy for the atmosphere:
$$ MSE = c_p ~T + g~ Z + L_v ~q  $$
whose terms are respectively the internal energy, the potential energy, and the latent heat of water vapor (see texts on atmopsheric thermodynamics for details).
We will assume that $MSE$ is a good approximation to the total energy content of the atmosphere, so
$$ e_a \approx MSE $$
Note that in both cases we have neglected the kinetic energy from this budget.
The kinetic energy per unit mass is $e_k = |\vec{v}|^2/2$, where $\vec{v} =...
Can you write Python code for it?

%load_ext version_information
%version_information numpy, scipy, matplotlib, xarray, climlab


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Discuss...  Large net fluxes over ocean, not over land.
Energy budget for the ocean
Using exactly the same reasoning we used for the whole climate system, we can write a budget for the OCEAN ONLY:
$$ \frac{\partial E_o}{\partial t} = -F_S - \frac{1}{2 \pi a^2  \cos\phi } \frac{\partial \mathcal{H_o}}{\partial \phi} $$
In principle it is possible to calculate $\mathcal{H}_o$ from this budget, analagously to how we calculated the total $\mathcal{H}$.
Assuming that

surface fluxes are well-known
the ocean heat storage is negligible (a big assumption!)

we can write
$$ \mathcal{H}o(\phi) = 2 \pi ~a^2 \int{-\pi/2}^{\phi} - \cos\phi^\prime ~ F_S d\phi^\prime  $$
where the minus sign account for the fact that we defined $F_S$ as positive up (out of the ocean).
Energy budget for the atmosphere
The net energy source to the atmosphere is the sum of the TOA flux and the surface flux. Thus we can write
$$ \frac{\partial E_a}{\partial t} = R_{TOA} + F_S - \frac{1}{2 \pi a^2  \cos\phi } \frac{\partial \mathcal{H_a}}{\partial \phi} $$
and we can similarly integrate to get the transport:
$$ \mathcal{H}a(\phi) = 2 \pi ~a^2 \int{-\pi/2}^{\phi} \cos\phi^\prime ~ \big( R_{TOA} + F_S \big) d\phi^\prime  $$
Note that these formulas ensure that $\mathcal{H} = \mathcal{H}_a + \mathcal{H}_o$.
Atmospheric water budget and latent heat transport
Water vapor contributes to the atmopsheric energy transport because energy consumed through evaporation is converted back to sensible heat wherever the vapor subsequently condenses.
If the evaporation and the condensation occur at different latitudes then there is a net transport of energy due to the movement of water vapor.
We can use the same kind of budget reasoning to compute this latent heat transport. But this time we will make a budget for water vapor only.
The only sources and sinks of water vapor to the atmosphere are surface evaporation and precipitation:
$$ L_v \frac{\partial Q}{\partial t} = L_v \big( Evap - Precip \big) - \frac{1}{2 \pi a^2  \cos\phi } \frac{\partial...
Can you write Python code for it?

def CESM_heat_transport(run):
    #  Take zonal averages of the necessary input fields
    fieldlist = ['FLNT','FSNT','LHFLX','SHFLX','FLNS','FSNS','PRECSC','PRECSL','QFLX','PRECC','PRECL']
    zon = run[fieldlist].mean(dim='lon')
    OLR = zon.FLNT
    ASR = zon.FSNT
    Rtoa = ASR - OLR  # net downwelling radiation
    #  surface energy budget terms, all defined as POSITIVE UP
    #    (from ocean to atmosphere)
    LHF = zon.LHFLX
    SHF = zon.SHFLX
    LWsfc = zon.FLNS
    SWsfc = -zon.FSNS
    SnowFlux =  ((zon.PRECSC + zon.PRECSL) *
                      const.rho_w * const.Lhfus)
    # net upward radiation from surface
    SurfaceRadiation = LWsfc + SWsfc
    # net upward surface heat flux
    SurfaceHeatFlux = SurfaceRadiation + LHF + SHF + SnowFlux
    # net heat flux into atmosphere
    Fatmin = Rtoa + SurfaceHeatFlux
    #  hydrological cycle, all terms in  kg/m2/s or mm/s
    Evap = zon.QFLX
    Precip = (zon.PRECC + zon.PRECL) * const.rho_w
    EminusP = Evap - Precip

    # heat transport terms
    HTmonthly = {}
    HTmonthly['total'] = inferred_heat_transport(Rtoa)
    HTmonthly['atm'] = inferred_heat_transport(Fatmin)
    HTmonthly['ocean'] = inferred_heat_transport(-SurfaceHeatFlux)
    HTmonthly['latent'] = inferred_heat_transport(EminusP*const.Lhvap) # atm. latent heat transport from moisture imbal.
    HTmonthly['dse'] = HTmonthly['atm'] - HTmonthly['latent']  # dry static energy transport as residual

    #  annual averages
    HTann = {}
    for name, value in HTmonthly.items():
        HTann[name] = value.mean(dim='time')

    return HTann, HTmonthly

#  Compute heat transport partition for both control and 2xCO2 simulations
HT_control, _ = CESM_heat_transport(atm_control)
HT_2xCO2, _ = CESM_heat_transport(atm_2xCO2)

fig = plt.figure(figsize=(16,6))
runs = [HT_control, HT_2xCO2]
N = len(runs)

for n, HT in enumerate([HT_control, HT_2xCO2]):
    ax = fig.add_subplot(1, N, n+1)
    ax.plot(lat_cesm, HT['total'], 'k-', label='total', linewidth=2)
    ax.plot(lat_cesm,...


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Essentially the same story as the reanalysis data: there is a surplus of energy across the tropics and a net energy deficit in mid- to high latitudes.
There are two locations where ASR = OLR, near about 35º in both hemispheres.

4. The energy budget for a zonal band

Let’s now consider a thin band of the climate system, of width $\delta \phi$ , and write down a careful energy budget for it.

Let $\mathcal{H}(\phi)$ be the total rate of northward energy transport across the latitude line $\phi$, measured in Watts (usually PW).
Let $T(\phi,t)$ be the zonal average surface temperature ("zonal average" = average around latitude circle).
We can write the energy budget as
$$ \frac{\partial E}{\partial t} = \text{energy in} - \text{energy out} $$
where $E$ is the total energy content of the column, which is useful to write as
$$ E = \int_{bottom}^{top} \rho ~ e ~ dz $$
and $e$ is the local enthalpy of the fluid, in units of J kg$^{-1}$. The integral energy content $E$ thus has units of J m$^{-2}$.
We have written the time tendency as a partial derivative now because $E$ varies in both space and time.
Now there are two energy sources and two energy sinks to think about:
Radiation and dynamics (horizontal transport)
$$ \frac{\partial E}{\partial t} = R_{TOA} - (\text{transport out} - \text{transport in})~/ ~\text{area of band} $$
where we define the net incoming radiation at the top of atmosphere as
$$ R_{TOA} = \text{ASR} - \text{OLR} = (1-\alpha) Q - \text{OLR} $$
The surface area of the latitude band is
$$ A = \text{Circumference} ~\times ~ \text{north-south width} $$
$$ A = 2 \pi a \cos \phi  ~ \times ~  a \delta \phi  $$
$$ A = 2 \pi a^2  \cos\phi ~ \delta\phi $$
We will denote the energy transport in and out of the band respectively as $\mathcal{H}(\phi), \mathcal{H}(\phi + \delta\phi)$
Then the budget can be written
$$ \frac{\partial E}{\partial t} = \text{ASR} - \text{OLR} - \frac{1}{2 \pi a^2  \cos\phi ~ \delta\phi} \Big(...
Can you write Python code for it?

def inferred_heat_transport(energy_in, lat=None, latax=None):
    '''Compute heat transport as integral of local energy imbalance.
    Required input:
        energy_in: energy imbalance in W/m2, positive in to domain
    As either numpy array or xarray.DataArray
    If using plain numpy, need to supply these arguments:
        lat: latitude in degrees
        latax: axis number corresponding to latitude in the data
            (axis over which to integrate)
    returns the heat transport in PW.
    Will attempt to return data in xarray.DataArray if possible.
    '''
    from scipy import integrate
    from climlab import constants as const
    if lat is None:
        try: lat = energy_in.lat
        except:
            raise InputError('Need to supply latitude array if input data is not self-describing.')
    lat_rad = np.deg2rad(lat)
    coslat = np.cos(lat_rad)
    field = coslat*energy_in
    if latax is None:
        try: latax = field.get_axis_num('lat')
        except:
            raise ValueError('Need to supply axis number for integral over latitude.')
    #  result as plain numpy array
    integral = integrate.cumtrapz(field, x=lat_rad, initial=0., axis=latax)
    result = (1E-15 * 2 * np.math.pi * const.a**2 * integral)
    if isinstance(field, xr.DataArray):
        result_xarray = field.copy()
        result_xarray.values = result
        return result_xarray
    else:
        return result


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Evidently there is a substantial net flux out to space in this dataset.
Before we can compute heat transport from this data, we need to balance the global data.
To do this requires making assumptions about the spatial distribution of the imbalance.
The simplest assumption we can make is that the imbalance is uniform across the Earth.
Can you write Python code for it?

Rtoa_ncep_balanced = Rtoa_ncep - imbal_ncep
newimbalance = float((Rtoa_ncep_balanced * weight_ncep).mean(dim='lat'))
print( 'The net downward TOA radiation flux after balancing the data is %0.2e W/m2.' %newimbalance)

fig, ax = plt.subplots()
ax.plot(lat_ncep, inferred_heat_transport(Rtoa_ncep_balanced))
ax.set_ylabel('PW')
ax.set_xticks(ticks)
ax.grid()
ax.set_title('Total northward heat transport inferred from NCEP reanalysis (after global balancing)')


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Here we use the timeave['insolation'] to plot the annual mean insolation.
(We know it is the annual average because the last call to model.integrate_years was for exactly 1 year)
Can you write Python code for it?

fig, ax = plt.subplots()
ax.plot(model.lat, model.timeave['insolation'])
ax.set_xlabel('Latitude')
ax.set_ylabel('Insolation (W/m2)')


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Indeed, the budget is (very nearly) closed everywhere. Each latitude is in energy balance, independent of every other column.

3. Observed and modeled TOA radiation budget

We are going to look at the (time average) TOA budget as a function of latitude to see how it differs from the RCE state we just plotted.
Ideally we would look at actual satellite observations of SW and LW fluxes. Instead, here we will use the NCEP Reanalysis for convenience.
But bear in mind that the radiative fluxes in the reanalysis are a model-generated product, they are not really observations.
TOA budget from NCEP Reanalysis
Can you write Python code for it?

# Get TOA radiative flux data from NCEP reanalysis
# downwelling SW
dswrf = xr.open_dataset(ncep_url + '/other_gauss/dswrf.ntat.mon.1981-2010.ltm.nc', decode_times=False)
#dswrf = xr.open_dataset(url + 'other_gauss/dswrf')
#  upwelling SW
uswrf = xr.open_dataset(ncep_url + '/other_gauss/uswrf.ntat.mon.1981-2010.ltm.nc', decode_times=False)
#uswrf = xr.open_dataset(url + 'other_gauss/uswrf')
#  upwelling LW
ulwrf = xr.open_dataset(ncep_url + '/other_gauss/ulwrf.ntat.mon.1981-2010.ltm.nc', decode_times=False)
#ulwrf = xr.open_dataset(url + 'other_gauss/ulwrf')

ASR = dswrf.dswrf - uswrf.uswrf
OLR = ulwrf.ulwrf

ASRzon = ASR.mean(dim=('time','lon'))
OLRzon = OLR.mean(dim=('time','lon'))

ticks = [-90, -60, -30, 0, 30, 60, 90]
fig, ax = plt.subplots()
ax.plot(ASRzon.lat, ASRzon, label='ASR')
ax.plot(OLRzon.lat, OLRzon, label='OLR')
ax.set_ylabel('W/m2')
ax.set_xlabel('Latitude')
ax.set_xlim(-90,90); ax.set_ylim(50,310)
ax.set_xticks(ticks);
ax.set_title('Observed annual mean radiation at TOA')
ax.legend(); ax.grid();


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Let's now use this to calculate the total northward heat transport from our control simulation with the CESM:
Can you write Python code for it?

fig, ax = plt.subplots()
ax.plot(lat_cesm, inferred_heat_transport(ASR_cesm_zon - OLR_cesm_zon))
ax.set_ylabel('PW')
ax.set_xticks(ticks)
ax.grid()
ax.set_title('Total northward heat transport inferred from CESM control simulation')


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Our integral does NOT go to zero at the North Pole!. This means that the global energy budget is NOT balanced in the reanalysis data.
Let's look at the global imbalance:
Can you write Python code for it?

#  global average of TOA radiation in reanalysis data
weight_ncep = np.cos(np.deg2rad(lat_ncep)) / np.cos(np.deg2rad(lat_ncep)).mean(dim='lat')
imbal_ncep = (Rtoa_ncep * weight_ncep).mean(dim='lat')
print( 'The net downward TOA radiation flux in NCEP renalysis data is %0.1f W/m2.' %imbal_ncep)


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
Our modeled RCE state is far too warm in the tropics, and too cold in the mid- to high latitudes.
Vertical structure of temperature: comparing RCE to observations
Can you write Python code for it?

#  Observed air temperature from NCEP reanalysis
## The NOAA ESRL server is shutdown! January 2019
ncep_air = xr.open_dataset( ncep_url + "pressure/air.mon.1981-2010.ltm.nc", decode_times=False)
#air = xr.open_dataset(url+'pressure/air')
#ncep_air = air.rename({'lev':'level'})
level_ncep_air = ncep_air.level
lat_ncep_air = ncep_air.lat
Tzon = ncep_air.air.mean(dim=('time','lon'))

#  Compare temperature profiles in RCE and observations
contours = np.arange(180., 350., 15.)

fig = plt.figure(figsize=(14,6))
ax1 = fig.add_subplot(1,2,1)
cax1 = ax1.contourf(lat_ncep_air, level_ncep_air, Tzon+const.tempCtoK, levels=contours)
fig.colorbar(cax1)
ax1.set_title('Observered temperature (K)')

ax2 = fig.add_subplot(1,2,2)
field = model.timeave['Tatm'].transpose()
cax2 = ax2.contourf(model.lat, model.lev, field, levels=contours)
fig.colorbar(cax2)
ax2.set_title('RCE temperature (K)')

for ax in [ax1, ax2]:
    ax.invert_yaxis()
    ax.set_xlim(-90,90)
    ax.set_xticks([-90, -60, -30, 0, 30, 60, 90])


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
The total heat transport is very nearly symmetric about the equator, with poleward transport of about 5 to 6 PW in both hemispheres.
The transport peaks in magnitude near 35º latitude, the same latitude where we found that ASR = OLR. This is no coincidence!
Equatorward of 35º (across the tropics) there is net heating by radiation and net cooling by dynamics. The opposite is true poleward of 35º.
What about the "observations", i.e. the reanalysis data?
We can try to do the same calculation.
Can you write Python code for it?

#  Need to flip the arrays because we want to start from the south pole
Rtoa_ncep = ASRzon-OLRzon
lat_ncep = ASRzon.lat
fig, ax = plt.subplots()
ax.plot(lat_ncep, inferred_heat_transport(Rtoa_ncep))
ax.set_ylabel('PW')
ax.set_xticks(ticks)
ax.grid()
ax.set_title('Total northward heat transport inferred from NCEP reanalysis')


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
This figure reveals something fairly obvious, but still worth thinking about:
Warm temperatures are correlated with high insolation. It's warm where the sun shines.
More specifically, we can see a few interesting details here:

The seasonal cycle is weakest in the tropics and strongest in the high latitudes.
The warmest temperatures occur slighly NORTH of the equator
The highest insolation occurs at the poles at summer solstice.

The local surface temperature does not correlate perfectly with local insolation for two reasons:

the climate system has heat capacity, which buffers some of the seasonal variations
the climate system moves energy around in space!

2. Calculating Radiative-Convective Equilibrium as a function of latitude

As a first step to understanding the effects of heat transport by fluid motions in the atmosphere and ocean, we can calculate what the surface temperature would be without any motion.
Let's calculate a radiative-convective equilibrium state for every latitude band.
Putting realistic insolation into an RCM
This code demonstrates how to create a model with both latitude and vertical dimensions.
Can you write Python code for it?

# A two-dimensional domain
state = climlab.column_state(num_lev=30, num_lat=40, water_depth=10.)
#  Specified relative humidity distribution
h2o = climlab.radiation.ManabeWaterVapor(name='Fixed Relative Humidity', state=state)
#  Hard convective adjustment
conv = climlab.convection.ConvectiveAdjustment(name='Convective Adjustment', state=state, adj_lapse_rate=6.5)
#  Daily insolation as a function of latitude and time of year
sun = climlab.radiation.DailyInsolation(name='Insolation', domains=state['Ts'].domain)
#  Couple the radiation to insolation and water vapor processes
rad = climlab.radiation.RRTMG(name='Radiation',
                             state=state,
                             specific_humidity=h2o.q,
                             albedo=0.125,
                             insolation=sun.insolation,
                             coszen=sun.coszen)
model = climlab.couple([rad,sun,h2o,conv], name='RCM')
print( model)

model.compute_diagnostics()

fig, ax = plt.subplots()
ax.plot(model.lat, model.insolation)
ax.set_xlabel('Latitude')
ax.set_ylabel('Insolation (W/m2)');


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
This new insolation process uses the same code we've already been working with to compute realistic distributions of insolation. Here we are using
climlab.radiation.DailyInsolation
but there is also
climlab.radiation.AnnualMeanInsolation
for models in which you prefer to suppress the seasonal cycle and prescribe a time-invariant insolation.
The following code will just integrate the model forward in four steps in order to get snapshots of insolation at the solstices and equinoxes.
Can you write Python code for it?

#  model is initialized on Jan. 1

#  integrate forward just under 1/4 year... should get about to the NH spring equinox
model.integrate_days(31+28+22)
Q_spring = model.insolation.copy()
#  Then forward to NH summer solstice
model.integrate_days(31+30+31)
Q_summer = model.insolation.copy()
#  and on to autumnal equinox
model.integrate_days(30+31+33)
Q_fall = model.insolation.copy()
#  and finally to NH winter solstice
model.integrate_days(30+31+30)
Q_winter = model.insolation.copy()

fig, ax = plt.subplots()
ax.plot(model.lat, Q_spring, label='Spring')
ax.plot(model.lat, Q_summer, label='Summer')
ax.plot(model.lat, Q_fall, label='Fall')
ax.plot(model.lat, Q_winter, label='Winter')
ax.legend()
ax.set_xlabel('Latitude')
ax.set_ylabel('Insolation (W/m2)');


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
We find that ASR does NOT balance OLR in most locations.
Across the tropics the absorbed solar radiation exceeds the longwave emission to space.  The tropics have a net gain of energy by radiation.
The opposite is true in mid- to high latitudes: the Earth is losing energy by net radiation to space at these latitudes.
TOA budget from the control CESM simulation
Can you write Python code for it?

#  same thing from CESM control simulation
datapath = "http://thredds.atmos.albany.edu:8080/thredds/dodsC/cesm/"
atm_control = xr.open_dataset( datapath + 'som_control/som_control.cam.h0.clim.nc', decode_times=False)
atm_2xCO2 = xr.open_dataset( datapath + 'som_2xCO2/som_2xCO2.cam.h0.clim.nc', decode_times=False)

lat_cesm = atm_control.lat
ASR_cesm = atm_control.FSNT
OLR_cesm = atm_control.FLNT

ASR_cesm_zon = ASR_cesm.mean(dim=('lon','time'))
OLR_cesm_zon = OLR_cesm.mean(dim=('lon','time'))

fig, ax = plt.subplots()
ax.plot(lat_cesm, ASR_cesm_zon, label='ASR')
ax.plot(lat_cesm, OLR_cesm_zon, label='OLR')
ax.set_ylabel('W/m2')
ax.set_xlabel('Latitude')
ax.set_xlim(-90,90); ax.set_ylim(50,310)
ax.set_xticks(ticks);
ax.set_title('CESM control simulation: Annual mean radiation at TOA')
ax.legend(); ax.grid();


I am working on the file "Lectures/Lecture17 -- Heat transport.ipynb".
The first task is:
We now get a physically sensible result (zero at both poles).
The heat transport is poleward everywhere, and very nearly anti-symmetric across the equator. The shape is very similar to what we found from the CESM simulation, with peaks near 35º.
However the magnitude of the peaks is substantially smaller. Does this indicate a shortcoming of the CESM simulation?
Probably not!
It turns out that our result here is very sensitive to the details of how we balance the radiation data.
As an exercise, you might try applying different corrections other than the globally uniform correction we used above. E.g. try weighting the tropics or the mid-latitudes more strongly.
An example of a recently published observational estimate of meridional heat transport
<img src='../images/Fasullo_Trenberth_2008b_Fig7.jpg'>

The ERBE period zonal mean annual cycle of the meridional energy transport in PW by (a) the atmosphere and ocean as inferred from ERBE $R_T$, NRA $\delta$A_E/$\delta$t, and GODAS $\delta$O_E/$\delta$t; (b) the atmosphere based on NRA; and (c) by the ocean as implied by ERBE + NRA $F_S$ and GODAS $\delta$O_E/$\delta$t. Stippling and hatching in (a)–(c) represent regions and times of year in which the standard deviation of the monthly mean values among estimates, some of which include the CERES period (see text), exceeds 0.5 and 1.0 PW, respectively. (d) The median annual mean transport by latitude for the total (gray), atmosphere (red), and ocean (blue) accompanied with the associated $\pm2\sigma$ range (shaded).

This is a reproduction of Figure 7 from Fasullo and Trenberth (2008), "The Annual Cycle of the Energy Budget. Part II: Meridional Structures and Poleward Transports", J. Climate 21, doi:10.1175/2007JCLI1936.1
This figure shows the breakdown of the heat transport by season as well as the partition between the atmosphere and ocean.
Focussing just on the total, annual transport in panel (d) (black curve), we see that is quite consistent with what we computed from the CESM simulation.

<a...
Can you write Python code for it?

# monthly climatologies for surface flux data from reanalysis
#  all defined as positive UP
ncep_nswrs = xr.open_dataset(ncep_url + "surface_gauss/nswrs.sfc.mon.1981-2010.ltm.nc", decode_times=False)
ncep_nlwrs = xr.open_dataset(ncep_url + "surface_gauss/nlwrs.sfc.mon.1981-2010.ltm.nc", decode_times=False)
ncep_shtfl = xr.open_dataset(ncep_url + "surface_gauss/shtfl.sfc.mon.1981-2010.ltm.nc", decode_times=False)
ncep_lhtfl = xr.open_dataset(ncep_url + "surface_gauss/lhtfl.sfc.mon.1981-2010.ltm.nc", decode_times=False)
#ncep_nswrs = xr.open_dataset(url + 'surface_gauss/nswrs')
#ncep_nlwrs = xr.open_dataset(url + 'surface_gauss/nlwrs')
#ncep_shtfl = xr.open_dataset(url + 'surface_gauss/shtfl')
#ncep_lhtfl = xr.open_dataset(url + 'surface_gauss/lhtfl')

#  Calculate ANNUAL AVERAGE net upward surface flux
ncep_net_surface_up = (ncep_nlwrs.nlwrs
                     + ncep_nswrs.nswrs
                     + ncep_shtfl.shtfl
                     + ncep_lhtfl.lhtfl
                      ).mean(dim='time')

lon_ncep = ncep_net_surface_up.lon
fig, ax = plt.subplots()
cax = ax.pcolormesh(lon_ncep, lat_ncep, ncep_net_surface_up,
               cmap=plt.cm.seismic, vmin=-200., vmax=200. )
fig.colorbar(cax, ax=ax)
ax.set_title('Net upward surface energy flux in NCEP Reanalysis data')