Author of this notebook: Maybritt Schillinger
Welcome! This is a little notebook to help you try some things with the Python package Climlab. The documentation can be found here: https://climlab.readthedocs.io/en/latest/intro.html
First, here are some setups and helper functions. No need for you to bother with this. These are just for importing the necessary libraries and defining our own class for an EBM model that we will use later.
from __future__ import division
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import climlab
from builtins import next
from climlab.process.diagnostic import DiagnosticProcess
from climlab.domain.field import Field, global_mean
class tanalbedo(DiagnosticProcess):
def __init__(self, **kwargs):
super(tanalbedo, self).__init__(**kwargs)
self.add_diagnostic('albedo')
Ts = self.state['Ts']
self._compute_fixed()
def _compute_fixed(self):
Ts = self.state['Ts']
try:
lon, lat = np.meshgrid(self.lon, self.lat)
except:
lat = self.lat
phi = lat
try:
albedo=np.zeros(len(phi));
albedo=0.42-0.20*np.tanh(0.052*(Ts-3))
except:
albedo = np.zeros_like(phi)
dom = next(iter(self.domains.values()))
self.albedo = Field(albedo, domain=dom)
def _compute(self):
self._compute_fixed()
return {}
class simple_EBM_seasonal(climlab.model.ebm.EBM_seasonal):
def __init__(self, orbit={'ecc': 0.0167643, 'long_peri': 280.32687, 'obliquity': 23.459277}, albedo='constant', Tf=-10.0, CO2=300, **kwargs):
super().__init__(**kwargs)
self.remove_subprocess('insolation')
self.remove_subprocess('SW')
self.remove_subprocess('albedo')
self.remove_subprocess('LW')
# catch model domain for subprocess creation
surface = self.domains['Ts']
# define new insolation and SW process
insolation = climlab.radiation.DailyInsolation(domains=surface, orb = orbit, **self.param)
# now define albedo process
if albedo=='constant':
alb = climlab.surface.albedo.ConstantAlbedo(domains=surface, **self.param)
elif albedo=='step':
alb = climlab.surface.albedo.StepFunctionAlbedo(state=self.state, Tf=Tf, **self.param)
elif albedo=='tanh':
alb = tanalbedo(state=self.state, **self.param)
else:
print('The given albedo option is not known. Will put in constant albedo.')
alb = climlab.surface.albedo.ConstantAlbedo(domains=surface, **self.param)
SW = climlab.radiation.absorbed_shorwave.SimpleAbsorbedShortwave(insolation=insolation.insolation, state = self.state, albedo = alb.albedo, **self.param)
LW = climlab.radiation.aplusbt.AplusBT_CO2(CO2=CO2, state=self.state, **self.param)
self.add_subprocess('LW', LW)
self.add_subprocess('insolation', insolation)
self.add_subprocess('SW', SW)
self.add_subprocess('albedo', alb)
self.compute_diagnostics()
def global_annual_mean(self):
num_steps_per_year = int(self.time['num_steps_per_year'])
mean_year = np.empty(num_steps_per_year)
for m in range(num_steps_per_year):
self.step_forward()
mean_year[m] = self.global_mean_temperature()
gam = np.mean(mean_year)
return gam
def plot_temp_rad(self):
# creating plot figure
fig = plt.figure(figsize=(15,5))
# Temperature plot
ax1 = fig.add_subplot(121)
ax1.plot(self.lat,self.Ts)
ax1.set_xticks([-90,-60,-30,0,30,60,90])
ax1.set_xlim([-90,90])
ax1.set_title('Surface Temperature', fontsize=14)
ax1.set_ylabel('(degC)', fontsize=12)
ax1.grid()
# Net Radiation plot
ax3 = fig.add_subplot(122, sharex = ax1)
ax3.plot(self.lat, self.OLR, label='OLR',
color='cyan')
ax3.plot(self.lat, self.ASR, label='ASR',
color='magenta')
ax3.plot(self.lat, self.ASR-self.OLR,
label='net radiation',
color='red')
ax3.set_title('Net Radiation', fontsize=14)
ax3.set_ylabel('(W/m$^2$)', fontsize=12)
ax3.legend(loc='best')
ax3.grid()
plt.show()
def plot_timeave(self):
# integrate for one year
self.integrate_years(1.)
# creating plot figure
fig = plt.figure(figsize=(15,5))
# Temperature plot
ax1 = fig.add_subplot(121)
ax1.plot(self.lat,self.timeave['Ts'])
ax1.set_xticks([-90,-60,-30,0,30,60,90])
ax1.set_xlim([-90,90])
ax1.set_title('Annual mean Surface Temperature', fontsize=14)
ax1.set_ylabel('(degC)', fontsize=12)
ax1.grid()
# Net Radiation plot
ax3 = fig.add_subplot(122, sharex = ax1)
ax3.plot(self.lat, self.timeave['OLR'], label='Annual mean OLR',
color='cyan')
ax3.plot(self.lat, self.timeave['ASR'], label='Annual mean ASR',
color='magenta')
ax3.plot(self.lat, self.timeave['ASR']-self.timeave['OLR'],
label='Annual mean net radiation',
color='red')
ax3.set_title('Annual mean Net Radiation', fontsize=14)
ax3.set_ylabel('(W/m$^2$)', fontsize=12)
ax3.legend(loc='best')
ax3.grid()
plt.show()
def plot_albedo(self):
fig = plt.figure(figsize=(7.5,5))
# Temperature plot
ax2 = fig.add_subplot(111)
ax2.plot(self.lat,self.albedo)
ax2.set_title('Albedo', fontsize=14)
ax2.set_xlabel('latitude', fontsize=10)
ax2.set_ylabel('', fontsize=12)
ax2.set_xticks([-90,-60,-30,0,30,60,90])
ax2.set_xlim([-90,90])
ax2.set_ylim([0,1])
ax2.grid()
plt.show()
def plot_temp_year(self):
num_steps_per_year = int(self.time['num_steps_per_year'])
Tyear = np.empty((self.lat.size, num_steps_per_year))
for m in range(num_steps_per_year):
self.step_forward()
Tyear[:,m] = np.squeeze(self.Ts)
Tmin=np.min(Tyear)
Tmax=np.max(Tyear)
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(111)
factor = 365. / num_steps_per_year
cax = ax.contourf(factor * np.arange(num_steps_per_year),
self.lat, Tyear[:,:],
cmap=plt.cm.seismic, vmin=Tmin, vmax=Tmax)
cbar1 = plt.colorbar(cax)
ax.set_title('Temperatures throughout the year', fontsize=14)
ax.set_xlabel('Days of year', fontsize=14)
ax.set_ylabel('Latitude', fontsize=14)
def plot_ice_year(self):
if 'Tf' in self.subprocess['albedo'].param.keys():
Tf = self.subprocess['albedo'].param['Tf']
else:
print('No ice considered in this model. Can not plot.')
return
num_steps_per_year = int(self.time['num_steps_per_year'])
ice_year = np.empty((self.lat.size, num_steps_per_year))
for m in range(num_steps_per_year):
self.step_forward()
ice_year[:,m] = np.where(np.squeeze(self.Ts) <= Tf, 0, 1)
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(111)
factor = 365. / num_steps_per_year
cax = ax.contourf(factor * np.arange(num_steps_per_year),
self.lat, ice_year[:,:],
cmap=plt.cm.seismic, vmin=0, vmax=1, levels=2)
cbar1 = plt.colorbar(cax)
ax.set_title('Ice throughout the year', fontsize=14)
ax.set_xlabel('Days of year', fontsize=14)
ax.set_ylabel('Latitude', fontsize=14)
Climlab is a flexible package, where you can define your own climate models by coupling together different processes. But it has certain types of models implemented already. Setting up a climate model in climlab can be as easy as this:
ebm = climlab.EBM()
Let's print what we just created:
print(ebm)
We see that the mdel has one state variable: Ts, that is the surface temperature. The domain where it is defined on has shape (90, 1), that means it has 90 points in latitudinal direction and 1 point in the depth-direction, i.e. downwards. So this is a 1D model now.
Important note: The model is assuming we just have a planet of water of uniform depth. We can get the water depth by ebm.param
, which also gives us some other parameters, for example S0, the solar constant.
ebm.param
The model consists of subprocesses. For example, it has a process 'LW' which is computing the outgoing longwave radiation or 'albedo' which is calculating the planetary albedo at each latitude, or 'diffusion' for heat transport. Furthermore, there is a process called 'insolation'. Currently, it is just approximating the annual mean insolation at each latitude by a polynomial which only depends on the latitude but not on the time. So there are no seasonal variations yet. This is the process we will adjust first:
Let us first look at a seasonal EBM. Climlab has a class EBM_seasonal
implemented, which we adjust for our purposes. We first change the insolation process as well as the process for absorbed shortwave radiation since that builds up on the insolation. Also, to simplify, we assume a constant albedo value (0.33). At the beginning we defined a class that does this for you, as well as defining some methods plotting. You don't need to worry about the details but if you are curious you can check out the source code at the top of this notebook.
You can now create a simple seasonal EBM by calling:
ebm_seasonal = simple_EBM_seasonal()
Again, print the model:
print(ebm_seasonal)
By default, this model was given a temperature profile, which we can plot using our own new method. We can also look at the radiations: OLR = outgoing longwave radiation. ASR = absorbed shortwave radiation. Call a plot function as follows:
ebm_seasonal.plot_temp_rad()
Observations: The OLR (in the right graph) is symmetric around the equator just like the temperature. This makes sense: in this model, OLR is modeled as a linear function of temperature. Note that the model starts at January 1, so it is winter on the northern hemisphere. At the north pole, there is no incoming sunlight.
Question: Where is the earth gaining energy and where is it loosing energy?
So far, this is just the initial state at the time of model creation. Now we let the model involve in time. We can step forward, for example for 30 days.
Question: What do you expect? Where will it get colder and where warmer?
To try this, run the following cell to see the output. In order to run that cell, the packages need to be loaded and the variables defined. In Jupyter Lab, there is an option Run -> Run All Above Selected Cell. In Jupyter Notebook, this is Cell -> Run All Above. Through this, the above cells are run and all the setups are done. After completing this, you can make your way downwards through the notebook by running the selected cell and advancing.
ebm_seasonal.integrate_days(30.)
ebm_seasonal.plot_temp_rad()
Task: Step forward further in time through additional calls of ebm_seasonal.integrate_days(some number of days)
and check those profiles at the equinoxes and the summer and winter solstices.
Hints:
ebm_seasonal.time
to print out some features. (Currently we have 90 timesteps per year, so this might lead to some non-integer numbers for the elapsed days and days of the year.)integrate_years()
to get closer to that equilibrium, for example ebm_seasonal.integrate_years(5.0)
plot_temp_year()
, for example ebm_seasonal.plot_temp_year()
. It will step forward for 365 days and and make a contour plot.# your code here
# suggestion
# start again at Jan 1
ebm_seasonal = simple_EBM_seasonal()
# first integrate forward some years to get closer to equilibrium
ebm_seasonal.integrate_years(5.0)
# look at March equinox
ebm_seasonal.integrate_days(31+28+20)
print('March 20')
ebm_seasonal.plot_temp_rad()
# look at summer solstice (June 20)
ebm_seasonal.integrate_days(31+30+31)
print('June 20')
ebm_seasonal.plot_temp_rad()
# September equinox (Sept 20)
ebm_seasonal.integrate_days(30+31+30)
print('Sept 20')
ebm_seasonal.plot_temp_rad()
# winter solstice (Dec 20)
ebm_seasonal.integrate_days(30+31+30)
print('Dec 20')
ebm_seasonal.plot_temp_rad()
# check where we are at during the year
ebm_seasonal.time
# we need five further steps to get to the start of the year again
for i in range(5):
ebm_seasonal.step_forward()
ebm_seasonal.time
# maybe now try to look at distribution throughout the year
ebm_seasonal.plot_temp_year()
Next, we want to learn more about the influence of different orbit paramters. The class simple_EBM_seasonal()
can take a parameter orbit
for initialisation. This should be a dictionary of the form orbit = {'ecc': value, 'long_peri': value, 'obliquity': value}
and by default it is set to the current values: orbit = {'ecc': 0.0167643, 'long_peri': 280.32687, 'obliquity': 23.459277}
.
# feel free to adjust these parameters later
orbit = {'ecc': 0.0167643, 'long_peri': 280.32687, 'obliquity': 23.459277}
ebm2 = simple_EBM_seasonal(orbit)
# step forward in time for some years so model is in equilibrium
ebm2.integrate_years(10.)
Task: You can now adjust orbit parameters and see how this influences the temperature profile and the radiations throughout the year.
Hints:
integrate_years()
automatically calculates the time averaged temperature. So if we run it for exactly one year, we get the annual mean temperature (and many other diagnostics) saved in the dictionary timeave
.The method plot_timeave()
of our own class simple_EBM_seasonal()
uses this: it calls integrate_years(1.)
once and then plots the quantities from timeave
. Example use:
ebm2.plot_timeave().
Climlab classes also have a method for computing the global mean temperature: global_mean_temperature()
. But this only refers to the one point in time where the model is currently at.
Hence, our own class includes another method global_annual_mean()
which steps forward in time and computes the global mean at each step, later computing the mean of these values to get the annual mean. Example usage:
print(ebm2.gobal_annual_mean())
# more code here
# step forward in time, use plotting functions or functions to compute the global, annual mean
# suggestions
# make some very tilted example
print('An example with obl = 90')
orbit = {'ecc': 0.0167643, 'long_peri': 280.32687, 'obliquity': 90}
ebm3 = simple_EBM_seasonal(orbit)
ebm3.integrate_years(10.)
ebm3.plot_timeave()
ebm3.plot_temp_year()
print('The global annual mean is {} \n'.format(ebm3.global_annual_mean()))
# what about no tilt at all?
print('An example with obl = 0')
orbit = {'ecc': 0.0167643, 'long_peri': 280.32687, 'obliquity': 0}
ebm4 = simple_EBM_seasonal(orbit)
ebm4.integrate_years(10.)
ebm4.plot_timeave()
ebm4.plot_temp_year()
print('The global annual mean is {}'.format(ebm4.global_annual_mean()))
# Now let's vary the eccentricity - what happens with a very eccentric orbit?
print('An example with ecc = 0.2')
orbit = {'ecc': 0.2, 'long_peri': 280.32687, 'obliquity': 23}
ebm5 = simple_EBM_seasonal(orbit)
ebm5.integrate_years(10.)
ebm5.plot_timeave()
ebm5.plot_temp_year()
print('The global annual mean is {}\n'.format(ebm5.global_annual_mean()))
# And on a circular orbit?
print('An example with ecc = 0')
orbit = {'ecc': 0, 'long_peri': 280.32687, 'obliquity': 23}
ebm6 = simple_EBM_seasonal(orbit)
ebm6.integrate_years(10.)
ebm6.plot_timeave()
ebm6.plot_temp_year()
print('The global annual mean is {}'.format(ebm6.global_annual_mean()))
# lastly, change the longitude of the perihelion
print('An example with long_peri = 0 and still an eccentric orbit')
orbit = {'ecc': 0.2, 'long_peri': 0, 'obliquity': 23}
ebm7 = simple_EBM_seasonal(orbit)
ebm7.integrate_years(10.)
ebm7.plot_timeave()
ebm7.plot_temp_year()
plt.show() # this line ensures that the plots and print statements are in the correct order
ebm7.integrate_days(31+28+20)
print('Looking at March equinox')
ebm7.plot_temp_rad()
print('The global annual mean is {}\n'.format(ebm7.global_annual_mean()))
# And on a circular orbit?
print('An example with long_peri = 180 and still an eccentric orbit')
orbit = {'ecc': 0.2, 'long_peri': 180, 'obliquity': 23}
ebm8 = simple_EBM_seasonal(orbit)
ebm8.integrate_years(10.)
ebm8.plot_timeave()
ebm8.plot_temp_year()
plt.show() # this line ensures that the plots and print statements are in the correct order
ebm8.integrate_days(31+28+20)
print('Looking at March equinox')
ebm8.plot_temp_rad()
print('The global annual mean is {}'.format(ebm8.global_annual_mean()))
So far, we've made a simplifying assumption: The albedo value was set to a constant. This can be seen by using the method plot_albedo()
of our class to plot the albedo.
ebm = simple_EBM_seasonal()
ebm.plot_albedo()
Now we will change this. Climlab implements a step function for the albedo value. At each timestep, the albedo is set depending on the temperature:
Note that changes between ice and no-ice are instantaneous, i.e. there is no melting process or built-up time for ice sheets.
The step function albedo can be included by setting albedo='step'
in the initialisation of our class. This gives the following picture for our initial temperature curve:
ebm_stepalb = simple_EBM_seasonal(albedo='step')
ebm_stepalb.plot_albedo()
Task: Try to repeat some of the above analysis for this new albedo function. How do the temperature distributions change throughout the year?
Hints:
plot_ice_year()
is a simple method to plot the ice distribution throughout the year. For example:
ebm_stepalb.plot_ice_year()
The threshold value specifying when the albedo is set to the ice-value can be changed by giving the argument Tf
to the constructor. For example
ebm_stepalb = simple_EBM_seasonal(albedo='step', Tf=-5.0).
Instead of using the step function albedo, you can also set albedo=tanh
for an implementation of the formula
albedo = 0.42 - 0.20 * np.tanh(0.052 * (Ts-3)).
# your code here
# step forward in time, use plotting functions, look at mean temperatures etc.
# maybe change the Tf parameter and see how this influences the ice and the temperatures
# suggestion
# old seasonal with constant albedo for reference
ebm_constalb = simple_EBM_seasonal(albedo='constant')
print('Constant albedo')
ebm_constalb.integrate_years(10.)
ebm_constalb.plot_timeave()
ebm_constalb.plot_temp_year()
plt.show() # this line ensures that the plots and print statements are in the correct order
print()
print('Step Function albedo')
ebm_stepalb.integrate_years(10.)
ebm_stepalb.plot_timeave()
ebm_stepalb.plot_temp_year()
ebm_stepalb.plot_ice_year()
# some example with more ice
ebm_ice = simple_EBM_seasonal(albedo='step', Tf = -3.0)
ebm_ice.integrate_years(10.)
ebm_ice.plot_timeave()
ebm_ice.plot_temp_year()
ebm_ice.plot_ice_year()
So far, we have played around with orbit parameters and albedo. Next, we will adjust the levels of CO2 in our atmosphere. The CO2 concentration (in ppm) can be changed by adjusting the parameter CO2, for example:
ebm_co2 = simple_EBM_seasonal(albedo='step', CO2=411)
to set it to the 2019 value.
Aside: The seasonal EBM in Climlab does NOT implement a complex radiation model. The relationship between temperature and outgoing longwave radiation is simply parametrised by
OLR = A + B * T,
where A and B depend on the CO2 concentration. If you want to use a more realistic radiation model, you will have to work with Climlab's column models. However, these do not have any horizontal heat transport.
Task: Adjust the CO2 value to todays value - or even higher to represent the future. Possibly integrate forward in time for some years to bring the model to equilibrium. See how the temperatures / ice / radiations change. Feel free to try different albedo parameters also.
Further suggestion: It could also be interesting to calculate a climate sensitivity, i.e. the temperature change from doubling the CO2 value in the atmosphere. You could define two models:
ebm_co2_low = simple_EBM_seasonal(albedo='step', CO2=280)
ebm_co2_high = simple_EBM_seasonal(albedo='step', CO2=560)
Then for each of them, call the integrate_years()
method for a few years, maybe around 10, and compare the global annual mean with
ebm_co2_low.global_annual_mean()
ebm_co2_high.global_annual_mean().
How much warmer did it get?
# your code here
# feel free to change the CO2 value or define several models with different concentrations and compare
# co2 concentration of today
print('Co2 concentration of today')
ebm_co2 = simple_EBM_seasonal(albedo='step', CO2=411)
ebm_co2.integrate_years(10.)
ebm_co2.plot_timeave()
ebm_co2.plot_temp_year()
ebm_co2.plot_ice_year()
plt.show()
print('The global annual mean is {}'.format(ebm_co2.global_annual_mean()))
print()
print('Co2 in 2100 in RCP 8.5 (BAU)')
# co2 concentration in 2100 with business as usual scenario
ebm_co2 = simple_EBM_seasonal(albedo='step', CO2=935)
ebm_co2.integrate_years(10.)
ebm_co2.plot_timeave()
ebm_co2.plot_temp_year()
ebm_co2.plot_ice_year()
plt.show()
print('The global annual mean is {}'.format(ebm_co2.global_annual_mean()))
Here is an overview of parameters for some past times:
Simulation Name | Time | CO2 value | Eccentricity | Obliquity | Precession |
---|---|---|---|---|---|
Pre-Industrial | 1800 | 284.6 | 0.0167643 | 23.459277 | 280.32687 |
Future | 560 | 0.0167643 | 23.459277 | 280.32687 | |
Last Interglacial | 127 k | 278 | 0.038231 | 24.2441 | 49.097 |
Mid-Holocene Warm Period | 6 k | 280 | 0.018682 | 24.1048 | 180.918 |
Task: You could try to feed those parameters into the model and see how temperatures were like in the past.
# your code here
# look at Last Interglacial
orbit = {'ecc': 0.038231, 'long_peri': 49.097, 'obliquity': 24.2441}
ebm = simple_EBM_seasonal(orbit, CO2=278, albedo='step')
ebm.integrate_years(10.)
ebm.plot_timeave()
ebm.plot_temp_year()
ebm.plot_ice_year()
As mentioned above, in Climlab we just have a planet of water of uniform water depth. The water depth is used to calculate the heat capacity. It is an interesting experiment to change the value and see how this effects seasonal variation. You can try this by specifying the parameter water_depth
when setting up the model, for example
ebm_deep = simple_EBM_seasonal(albedo='step', water_depth=100).
Task: Define different models with different water depths and plot the seasonal changes with plot_timeave()
or plot_ice_year()
(the latter only if you used albedo='step'
). Compare.
# your code here
ebm_deep = simple_EBM_seasonal(albedo='step', water_depth=100)
ebm_deep.integrate_years(10.)
ebm_deep.plot_temp_year()