OpenSCM Two Layer Model¶
OpenSCM two layer model contains implementations of the two layer radiative forcing driven models by Held et al., Geoffroy et al. and Bloch-Johnson et al.
OpenSCM Two Layer Model was designed to provide a clean, modularised, extensible interface for one of the most commonly used simple climate models. It was used in Phase 1 of the Reduced Complexity Model Intercomparison Project as a point of comparison for the other participating models.
The FaIR model implements a mathematically equivalent model (under certain assumptions) but does not provide as clear a conversion between the two-layer model and the two-timescale response as is provided here. We hope that this implementation could interface with other simple climate models like FaIR to allow simpler exploration of the combined behaviour of interacting climate components with minimal coupling headaches.
As implemented here, the “OpenSCM Two Layer Model” interface is target at researchers and as an education tool. Users from other fields are of course encouraged to use it if they find it helpful.
OpenSCM two layer model is free software under a BSD 3-Clause License, see LICENSE.
If you have any issues or would like to discuss a feature request, please raise them in the OpenSCM Two Layer Model issue tracker. If your issue is a feature request or a bug, please use the templates available, otherwise, simply open a normal issue.
Installation¶
OpenSCM two layer model has only two dependencies:
scmdata>=0.9
tqdm
OpenSCM two layer model can be installed with pip
pip install openscm-twolayermodel
If you also want to run the example notebooks install additional dependencies using
pip install "openscm-twolayermodel[notebooks]"
Coming soon OpenSCM two layer model can also be installed with conda
conda install -c conda-forge openscm-twolayermodel
We do not ship our tests with the packages. If you wish to run the tests, you must install from source (or download the tests separately and run them on your installation).
Installing from source¶
To install from source, simply clone the repository and then install it using pip e.g. pip install ".[dev]"
.
Having done this, the tests can be run with pytest tests
or using the Makefile
(make test
will run only the code tests, make checks
will run the code tests and all other tests e.g. linting, notebooks, documentation).
For more details, see the development section of the docs.
Usage¶
Here we provide examples of OpenSCM two layer model’s behaviour and usage. The source code of these usage examples is available in the folder docs/source/usage of the GitHub repository.
Basic demos¶
Getting Started¶
This notebook demonstrates the OpenSCM Two Layer Model repository’s basic functionality.
We start with imports, their need will become clearer throughout the notebook.
[1]:
import inspect
import numpy as np
from openscm_units import unit_registry
from scmdata import ScmRun
import openscm_twolayermodel
from openscm_twolayermodel import ImpulseResponseModel, TwoLayerModel
from openscm_twolayermodel.base import Model
/home/docs/checkouts/readthedocs.org/user_builds/openscm-two-layer-model/envs/stable/lib/python3.7/site-packages/openscm_twolayermodel/base.py:10: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
import tqdm.autonotebook as tqdman
As with most Python packages, the version of openscm_twolayermodel
being used can always be checked as shown below. This is very helpful for debugging.
[2]:
# NBVAL_IGNORE_OUTPUT
openscm_twolayermodel.__version__
[2]:
'0.2.3'
OpenSCM Two Layer Model has two key classes: ImpulseResponseModel
and TwoLayerModel
. These are implementations of the two major variants of the two-layer model found in the literature. We can see that they both have a common base class using the inspect
package.
[3]:
inspect.getmro(ImpulseResponseModel)
[3]:
(openscm_twolayermodel.impulse_response_model.ImpulseResponseModel,
openscm_twolayermodel.base.TwoLayerVariant,
openscm_twolayermodel.base.Model,
abc.ABC,
object)
[4]:
inspect.getmro(TwoLayerModel)
[4]:
(openscm_twolayermodel.two_layer_model.TwoLayerModel,
openscm_twolayermodel.base.TwoLayerVariant,
openscm_twolayermodel.base.Model,
abc.ABC,
object)
These classes can both be used in the same way. We demonstrate the most basic usage here, more comprehensive usage is demonstrated in other notebooks.
The first thing we need is our effective radiative forcing driver. This should be an `ScmRun
<https://scmdata.readthedocs.io/en/latest/data.html#the-scmrun-class>`__ instance.
[5]:
run_length = 200
driver = ScmRun(
data=np.arange(run_length) * 4 / 70,
index=1850 + np.arange(run_length),
columns={
"unit": "W/m^2",
"model": "idealised",
"scenario": "1pctCO2",
"region": "World",
"variable": "Effective Radiative Forcing",
},
)
driver
[5]:
<scmdata.ScmRun (timeseries: 1, timepoints: 200)>
Time:
Start: 1850-01-01T00:00:00
End: 2049-01-01T00:00:00
Meta:
model region scenario unit variable
0 idealised World 1pctCO2 W/m^2 Effective Radiative Forcing
[6]:
# NBVAL_IGNORE_OUTPUT
driver.lineplot()
[6]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

Then we can initialise instances of our models and run them.
[7]:
# NBVAL_IGNORE_OUTPUT
two_layer = TwoLayerModel(lambda0=4 / 3 * unit_registry("W/m^2/delta_degC"))
res_two_layer = two_layer.run_scenarios(driver)
impulse_response = ImpulseResponseModel(d1=10 * unit_registry("yr"))
res_impulse_response = impulse_response.run_scenarios(driver)
res = res_two_layer.append(res_impulse_response)
res.head()
[7]:
time | 1850-01-01 | 1851-01-01 | 1852-01-01 | 1853-01-01 | 1854-01-01 | 1855-01-01 | 1856-01-01 | 1857-01-01 | 1858-01-01 | 1859-01-01 | ... | 2040-01-01 | 2041-01-01 | 2042-01-01 | 2043-01-01 | 2044-01-01 | 2045-01-01 | 2046-01-01 | 2047-01-01 | 2048-01-01 | 2049-01-01 | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a (watt / delta_degree_Celsius ** 2 / meter ** 2) | climate_model | d1 (a) | d2 (a) | dl (meter) | du (meter) | efficacy (dimensionless) | eta (watt / delta_degree_Celsius / meter ** 2) | lambda0 (watt / delta_degree_Celsius / meter ** 2) | model | q1 (delta_degree_Celsius * meter ** 2 / watt) | q2 (delta_degree_Celsius * meter ** 2 / watt) | region | run_idx | scenario | unit | variable | |||||||||||||||||||||
0.0 | two_layer | NaN | NaN | 1200.0 | 50.0 | 1.0 | 0.8 | 1.333333 | idealised | NaN | NaN | World | 0 | 1pctCO2 | W/m^2 | Effective Radiative Forcing | 0.0 | 0.057143 | 0.114286 | 0.171429 | 0.228571 | 0.285714 | 0.342857 | 0.400000 | 0.457143 | 0.514286 | ... | 10.857143 | 10.914286 | 10.971429 | 11.028571 | 11.085714 | 11.142857 | 11.200000 | 11.257143 | 11.314286 | 11.371429 |
delta_degC | Surface Temperature|Upper | 0.0 | 0.000000 | 0.008626 | 0.023100 | 0.041545 | 0.062689 | 0.085676 | 0.109924 | 0.135040 | 0.160761 | ... | 5.710809 | 5.744627 | 5.778474 | 5.812348 | 5.846251 | 5.880182 | 5.914140 | 5.948127 | 5.982141 | 6.016183 | |||||||||||||||
Surface Temperature|Lower | 0.0 | 0.000000 | 0.000000 | 0.000043 | 0.000159 | 0.000368 | 0.000681 | 0.001109 | 0.001656 | 0.002328 | ... | 1.937427 | 1.956414 | 1.975476 | 1.994612 | 2.013823 | 2.033107 | 2.052465 | 2.071897 | 2.091402 | 2.110980 | ||||||||||||||||
W/m^2 | Heat Uptake | 0.0 | 0.000000 | 0.057143 | 0.102784 | 0.140628 | 0.173178 | 0.202129 | 0.228623 | 0.253435 | 0.277089 | ... | 3.230641 | 3.242731 | 3.254783 | 3.266797 | 3.278774 | 3.290713 | 3.302615 | 3.314480 | 3.326307 | 3.338098 | |||||||||||||||
NaN | two_timescale_impulse_response | 10.0 | 400.0 | NaN | NaN | 1.0 | NaN | NaN | idealised | 0.3 | 0.4 | World | 0 | 1pctCO2 | W/m^2 | Effective Radiative Forcing | 0.0 | 0.057143 | 0.114286 | 0.171429 | 0.228571 | 0.285714 | 0.342857 | 0.400000 | 0.457143 | 0.514286 | ... | 10.857143 | 10.914286 | 10.971429 | 11.028571 | 11.085714 | 11.142857 | 11.200000 | 11.257143 | 11.314286 | 11.371429 |
5 rows × 200 columns
Now we can plot our outputs and compare (of course, we can make these two models the same if we’re clever about how we set the parameters, see the impulse response equivalence notebook).
[8]:
# NBVAL_IGNORE_OUTPUT
res.filter(variable="Surface Temperature*").lineplot(
hue="climate_model", style="variable"
)
[8]:
<AxesSubplot:xlabel='time', ylabel='delta_degC'>

[9]:
# NBVAL_IGNORE_OUTPUT
res.filter(variable="Heat*").lineplot(hue="climate_model", style="variable")
[9]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

Running scenarios¶
Here we show how multiple scenarios can be run using the OpenSCM Two Layer Model package.
[1]:
# NBVAL_IGNORE_OUTPUT
import os.path
import numpy as np
import pandas as pd
from openscm_units import unit_registry as ur
import tqdm.autonotebook as tqdman
from scmdata import ScmRun, run_append
from openscm_twolayermodel import TwoLayerModel
import matplotlib.pyplot as plt
/home/docs/checkouts/readthedocs.org/user_builds/openscm-two-layer-model/envs/stable/lib/python3.7/site-packages/ipykernel_launcher.py:7: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
import sys
For this we use RCMIP effective radiative forcing data.
[2]:
DATA_PATH = os.path.join(
"..",
"..",
"..",
"tests",
"test-data",
"rcmip-radiative-forcing-annual-means-v4-0-0.csv",
)
DATA_PATH
[2]:
'../../../tests/test-data/rcmip-radiative-forcing-annual-means-v4-0-0.csv'
[3]:
# NBVAL_IGNORE_OUTPUT
scenarios = ScmRun(DATA_PATH, lowercase_cols=True).filter(
scenario="historical", keep=False
)
scenarios
[3]:
<scmdata.ScmRun (timeseries: 480, timepoints: 751)>
Time:
Start: 1750-01-01T00:00:00
End: 2500-01-01T00:00:00
Meta:
activity_id mip_era model region scenario unit \
0 not_applicable CMIP5 AIM World rcp60 W/m^2
1 not_applicable CMIP5 AIM World rcp60 W/m^2
2 not_applicable CMIP5 AIM World rcp60 W/m^2
3 not_applicable CMIP5 AIM World rcp60 W/m^2
4 not_applicable CMIP5 AIM World rcp60 W/m^2
.. ... ... ... ... ... ...
494 not_applicable CMIP5 unspecified World historical-cmip5 W/m^2
495 not_applicable CMIP5 unspecified World historical-cmip5 W/m^2
496 not_applicable CMIP5 unspecified World historical-cmip5 W/m^2
497 not_applicable CMIP5 unspecified World historical-cmip5 W/m^2
498 not_applicable CMIP5 unspecified World historical-cmip5 W/m^2
variable
0 Radiative Forcing
1 Radiative Forcing|Anthropogenic
2 Radiative Forcing|Anthropogenic|Aerosols
3 Radiative Forcing|Anthropogenic|Aerosols|Aeros...
4 Radiative Forcing|Anthropogenic|Aerosols|Aeros...
.. ...
494 Radiative Forcing|Anthropogenic|Stratospheric ...
495 Radiative Forcing|Anthropogenic|Tropospheric O...
496 Radiative Forcing|Natural
497 Radiative Forcing|Natural|Solar
498 Radiative Forcing|Natural|Volcanic
[480 rows x 7 columns]
We can then run them, for a number of parameter settings, as shown.
[4]:
# NBVAL_IGNORE_OUTPUT
a_values = np.array([0, 0.01]) * ur("W/m^2/delta_degC^2")
a_values
[4]:
Magnitude | [0.0 0.01] |
---|---|
Units | watt/(delta_degree_Celsius2 meter2) |
[5]:
# NBVAL_IGNORE_OUTPUT
runner = TwoLayerModel()
output = []
for a in tqdman.tqdm(a_values, desc="Parameter settings"):
runner.a = a
output.append(runner.run_scenarios(scenarios))
output = run_append(output)
output
[5]:
<scmdata.ScmRun (timeseries: 80, timepoints: 751)>
Time:
Start: 1750-01-01T00:00:00
End: 2500-01-01T00:00:00
Meta:
a (watt / delta_degree_Celsius ** 2 / meter ** 2) activity_id \
0 0.00 not_applicable
1 0.00 not_applicable
2 0.00 not_applicable
3 0.00 not_applicable
4 0.00 not_applicable
.. ... ...
75 0.01 not_applicable
76 0.01 not_applicable
77 0.01 not_applicable
78 0.01 not_applicable
79 0.01 not_applicable
climate_model dl (meter) du (meter) efficacy (dimensionless) \
0 two_layer 1200 50 1.0
1 two_layer 1200 50 1.0
2 two_layer 1200 50 1.0
3 two_layer 1200 50 1.0
4 two_layer 1200 50 1.0
.. ... ... ... ...
75 two_layer 1200 50 1.0
76 two_layer 1200 50 1.0
77 two_layer 1200 50 1.0
78 two_layer 1200 50 1.0
79 two_layer 1200 50 1.0
eta (watt / delta_degree_Celsius / meter ** 2) \
0 0.8
1 0.8
2 0.8
3 0.8
4 0.8
.. ...
75 0.8
76 0.8
77 0.8
78 0.8
79 0.8
lambda0 (watt / delta_degree_Celsius / meter ** 2) mip_era model \
0 1.246667 CMIP6 AIM/CGE
1 1.246667 CMIP6 AIM/CGE
2 1.246667 CMIP6 AIM/CGE
3 1.246667 CMIP6 AIM/CGE
4 1.246667 CMIP6 AIM/CGE
.. ... ... ...
75 1.246667 CMIP6 REMIND-MAGPIE
76 1.246667 CMIP6 REMIND-MAGPIE
77 1.246667 CMIP6 REMIND-MAGPIE
78 1.246667 CMIP6 REMIND-MAGPIE
79 1.246667 CMIP6 REMIND-MAGPIE
region run_idx scenario unit \
0 World 0 ssp370 W/m^2
1 World 0 ssp370 delta_degC
2 World 0 ssp370 delta_degC
3 World 0 ssp370 W/m^2
4 World 1 ssp370-lowNTCF-aerchemmip W/m^2
.. ... ... ... ...
75 World 8 ssp534-over W/m^2
76 World 9 ssp585 W/m^2
77 World 9 ssp585 delta_degC
78 World 9 ssp585 delta_degC
79 World 9 ssp585 W/m^2
variable
0 Effective Radiative Forcing
1 Surface Temperature|Upper
2 Surface Temperature|Lower
3 Heat Uptake
4 Effective Radiative Forcing
.. ...
75 Heat Uptake
76 Effective Radiative Forcing
77 Surface Temperature|Upper
78 Surface Temperature|Lower
79 Heat Uptake
[80 rows x 15 columns]
[6]:
# NBVAL_IGNORE_OUTPUT
pkwargs = dict(
hue="scenario", style="a (watt / delta_degree_Celsius ** 2 / meter ** 2)",
)
fig = plt.figure(figsize=(12, 18))
ax = fig.add_subplot(211)
output.filter(variable="Surface Temperature|Upper").lineplot(**pkwargs, ax=ax)
ax = fig.add_subplot(212)
output.filter(variable="Heat Uptake").lineplot(**pkwargs, ax=ax)
[6]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

More detail¶
Impulse response equivalence¶
In this notebook we explore the equivalence between the two-layer model and a two-timescale impulse response approach.
Background¶
Following Geoffroy et al., 2013, Part 2, with notation altered to match our implementation, the two-layer model with efficacy (\(\epsilon\)) and state-dependent climate feedback can be written as
If the state-dependent feedback factor, \(a\), is non-zero, the two-layer model and impulse response approaches are not equivalent. However, if \(a=0\), they become the same.
Hereafter we assume \(a=0\), however this assumption should not be forgotten. In the case \(a=0\), the two-layer model can be written (adding an \(\epsilon\) for the deep-ocean equation too for simplicity later).
In matrix notation we have
where \(\mathbf{X} = \begin{pmatrix}T \\T_D\end{pmatrix}\), \(\mathbf{A} = \begin{bmatrix} - \frac{\lambda_0 + \epsilon \eta}{C} & \frac{\epsilon \eta}{C} \\ \frac{\epsilon \eta}{\epsilon C_d} & -\frac{\epsilon \eta}{\epsilon C_d} \end{bmatrix}\) and \(\mathbf{B} = \begin{pmatrix} \frac{F}{C} \\ 0 \end{pmatrix}\).
As shown in Geoffroy et al., 2013, Part 1, \(\mathbf{A}\) can be diagonalised i.e. written in the form \(\mathbf{A} = \mathbf{\Phi} \mathbf{D} \mathbf{\Phi}^{-1}\), where \(\mathbf{D}\) is a diagonal matrix. Applying the solution given in Geoffroy et al., 2013, Part 1 to our impulse response notation, we have
and
where
Given this, we can re-write the system as
Defining \(\mathbf{Y} = \begin{pmatrix} T_1 \\ T_2 \end{pmatrix}\), we have
or,
Re-writing, we have,
We can compare this to the notation of Millar et al., 2017 and see that
Hence we have redemonstrated the equivalence of the two-layer model and a two-timescale impulse response model. Given the parameters of the two-layer model, we can now trivially derive the equivalent parameters of the two-timescale model. Doing the reverse is possible, but requires some more work in order to make a useable route drop out.
The first step is to follow Geoffroy et al., 2013, Part 1, and define two extra constants
These constants have the useful property that \(a_1 + a_2 = 1\) (proof in Appendix A).
From above, we also see that
Hence
Next we calculate \(C\) via
We then use further relationships from Table 1 of Geoffroy et al., 2013, Part 1 (proof is left to the reader) to calculate the rest of the constants.
Firstly,
and then finally,
The final thing to notice here is that \(C_D\), \(\epsilon\) and \(\eta\) are not uniquely-defined. This makes sense, as shown by Geoffroy et al., 2013, Part 2, the introduction of the efficacy factor does not alter the behaviour of the system (it is still the same mathematical system) and so it is impossible for simply the two-timescale temperature response to uniquely define all three of these quantities. It can only define the products \(\epsilon C_D\) and \(\epsilon \eta\). Hence when translating from the two-timescale model to the two-layer model with efficacy, an explicit choice for the efficacy must be made. This does not alter the temperature response but it does alter the implied ocean heat uptake of the two-timescale model.
Long story short, when deriving two-layer model parameters from a two-timescale model, one must specify the efficacy.
Given that \(\mathbf{Y} = \mathbf{\Phi}^{-1} \mathbf{X}\) i.e. \(\mathbf{X} = \mathbf{\Phi} \mathbf{Y}\), we can also relate the impulse response boxes to the two layers.
Finally, the equivalent of the two-timescale and two-layer models allows us to also calculate the heat uptake of a two-timescale impulse response model. It is given by
Running the code¶
Here we actually run the two implementations to explore their similarity.
[1]:
import datetime as dt
import numpy as np
import pandas as pd
from openscm_units import unit_registry as ur
from scmdata.run import ScmRun, run_append
from openscm_twolayermodel import ImpulseResponseModel, TwoLayerModel
import matplotlib.pyplot as plt
/home/docs/checkouts/readthedocs.org/user_builds/openscm-two-layer-model/envs/stable/lib/python3.7/site-packages/openscm_twolayermodel/base.py:10: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
import tqdm.autonotebook as tqdman
First we define a scenario to run.
[2]:
time = np.arange(1750, 2501)
forcing = 0.05 * np.sin(time / 15 * 2 * np.pi) + 3.0 * time / time.max()
inp = ScmRun(
data=forcing,
index=time,
columns={
"scenario": "test_scenario",
"model": "unspecified",
"climate_model": "junk input",
"variable": "Effective Radiative Forcing",
"unit": "W/m^2",
"region": "World",
},
)
inp
[2]:
<scmdata.ScmRun (timeseries: 1, timepoints: 751)>
Time:
Start: 1750-01-01T00:00:00
End: 2500-01-01T00:00:00
Meta:
climate_model model region scenario unit \
0 junk input unspecified World test_scenario W/m^2
variable
0 Effective Radiative Forcing
[3]:
# NBVAL_IGNORE_OUTPUT
inp.lineplot()
[3]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

Next we run the two-layer model. In order for it to be convertible to a two-timescale model, we must turn state-dependence off (a=0).
[4]:
two_layer_config = {
"du": 55 * ur("m"),
"efficacy": 1.2 * ur("dimensionless"),
# "efficacy": 1.0 * ur("dimensionless"),
"a": 0 * ur("W/m^2/delta_degC^2"),
}
[5]:
# NBVAL_IGNORE_OUTPUT
twolayer = TwoLayerModel(**two_layer_config)
res_twolayer = twolayer.run_scenarios(inp)
res_twolayer
[5]:
<scmdata.ScmRun (timeseries: 4, timepoints: 751)>
Time:
Start: 1750-01-01T00:00:00
End: 2500-01-01T00:00:00
Meta:
a (watt / delta_degree_Celsius ** 2 / meter ** 2) climate_model \
0 0.0 two_layer
1 0.0 two_layer
2 0.0 two_layer
3 0.0 two_layer
dl (meter) du (meter) efficacy (dimensionless) \
0 1200 55 1.2
1 1200 55 1.2
2 1200 55 1.2
3 1200 55 1.2
eta (watt / delta_degree_Celsius / meter ** 2) \
0 0.8
1 0.8
2 0.8
3 0.8
lambda0 (watt / delta_degree_Celsius / meter ** 2) model region \
0 1.246667 unspecified World
1 1.246667 unspecified World
2 1.246667 unspecified World
3 1.246667 unspecified World
run_idx scenario unit variable
0 0 test_scenario W/m^2 Effective Radiative Forcing
1 0 test_scenario delta_degC Surface Temperature|Upper
2 0 test_scenario delta_degC Surface Temperature|Lower
3 0 test_scenario W/m^2 Heat Uptake
[6]:
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(16, 9))
ax = fig.add_subplot(121)
res_twolayer.filter(variable="*Temperature*").lineplot(hue="variable", ax=ax)
ax = fig.add_subplot(122)
res_twolayer.filter(variable="Heat Uptake").lineplot(hue="variable", ax=ax)
[6]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

Next we get the parameters with which we get the equivalent impulse response model.
[7]:
two_timescale_paras = twolayer.get_impulse_response_parameters()
two_timescale_paras
[7]:
{'d1': 103454323.57029569 <Unit('joule / watt')>,
'd2': 11181891933.114195 <Unit('joule / watt')>,
'q1': 0.4465999986742509 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'q2': 0.3555390387589074 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'efficacy': 1.2 <Unit('dimensionless')>}
[8]:
# NBVAL_IGNORE_OUTPUT
impulse_response = ImpulseResponseModel(**two_timescale_paras)
res_impulse_response = impulse_response.run_scenarios(inp)
res_impulse_response
[8]:
<scmdata.ScmRun (timeseries: 5, timepoints: 751)>
Time:
Start: 1750-01-01T00:00:00
End: 2500-01-01T00:00:00
Meta:
climate_model d1 (joule / watt) d2 (joule / watt) \
0 two_timescale_impulse_response 1.034543e+08 1.118189e+10
1 two_timescale_impulse_response 1.034543e+08 1.118189e+10
2 two_timescale_impulse_response 1.034543e+08 1.118189e+10
3 two_timescale_impulse_response 1.034543e+08 1.118189e+10
4 two_timescale_impulse_response 1.034543e+08 1.118189e+10
efficacy (dimensionless) model \
0 1.2 unspecified
1 1.2 unspecified
2 1.2 unspecified
3 1.2 unspecified
4 1.2 unspecified
q1 (delta_degree_Celsius * meter ** 2 / watt) \
0 0.4466
1 0.4466
2 0.4466
3 0.4466
4 0.4466
q2 (delta_degree_Celsius * meter ** 2 / watt) region run_idx \
0 0.355539 World 0
1 0.355539 World 0
2 0.355539 World 0
3 0.355539 World 0
4 0.355539 World 0
scenario unit variable
0 test_scenario W/m^2 Effective Radiative Forcing
1 test_scenario delta_degC Surface Temperature|Box 1
2 test_scenario delta_degC Surface Temperature|Box 2
3 test_scenario delta_degC Surface Temperature
4 test_scenario W/m^2 Heat Uptake
[9]:
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(16, 9))
ax = fig.add_subplot(121)
res_impulse_response.filter(variable="*Temperature*").lineplot(
hue="variable", ax=ax
)
ax = fig.add_subplot(122)
res_impulse_response.filter(variable="Heat Uptake").lineplot(
hue="variable", ax=ax
)
[9]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

We can compare the two responses as well.
[10]:
# NBVAL_IGNORE_OUTPUT
combined = run_append([res_impulse_response, res_twolayer])
combined
[10]:
<scmdata.ScmRun (timeseries: 9, timepoints: 751)>
Time:
Start: 1750-01-01T00:00:00
End: 2500-01-01T00:00:00
Meta:
a (watt / delta_degree_Celsius ** 2 / meter ** 2) \
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
5 0.0
6 0.0
7 0.0
8 0.0
climate_model d1 (joule / watt) d2 (joule / watt) \
0 two_timescale_impulse_response 1.034543e+08 1.118189e+10
1 two_timescale_impulse_response 1.034543e+08 1.118189e+10
2 two_timescale_impulse_response 1.034543e+08 1.118189e+10
3 two_timescale_impulse_response 1.034543e+08 1.118189e+10
4 two_timescale_impulse_response 1.034543e+08 1.118189e+10
5 two_layer NaN NaN
6 two_layer NaN NaN
7 two_layer NaN NaN
8 two_layer NaN NaN
dl (meter) du (meter) efficacy (dimensionless) \
0 NaN NaN 1.2
1 NaN NaN 1.2
2 NaN NaN 1.2
3 NaN NaN 1.2
4 NaN NaN 1.2
5 1200.0 55.0 1.2
6 1200.0 55.0 1.2
7 1200.0 55.0 1.2
8 1200.0 55.0 1.2
eta (watt / delta_degree_Celsius / meter ** 2) \
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
5 0.8
6 0.8
7 0.8
8 0.8
lambda0 (watt / delta_degree_Celsius / meter ** 2) model \
0 NaN unspecified
1 NaN unspecified
2 NaN unspecified
3 NaN unspecified
4 NaN unspecified
5 1.246667 unspecified
6 1.246667 unspecified
7 1.246667 unspecified
8 1.246667 unspecified
q1 (delta_degree_Celsius * meter ** 2 / watt) \
0 0.4466
1 0.4466
2 0.4466
3 0.4466
4 0.4466
5 NaN
6 NaN
7 NaN
8 NaN
q2 (delta_degree_Celsius * meter ** 2 / watt) region run_idx \
0 0.355539 World 0
1 0.355539 World 0
2 0.355539 World 0
3 0.355539 World 0
4 0.355539 World 0
5 NaN World 0
6 NaN World 0
7 NaN World 0
8 NaN World 0
scenario unit variable
0 test_scenario W/m^2 Effective Radiative Forcing
1 test_scenario delta_degC Surface Temperature|Box 1
2 test_scenario delta_degC Surface Temperature|Box 2
3 test_scenario delta_degC Surface Temperature
4 test_scenario W/m^2 Heat Uptake
5 test_scenario W/m^2 Effective Radiative Forcing
6 test_scenario delta_degC Surface Temperature|Upper
7 test_scenario delta_degC Surface Temperature|Lower
8 test_scenario W/m^2 Heat Uptake
To within numerical errors they are equal.
[11]:
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(16, 9))
ax = fig.add_subplot(121)
combined.filter(variable="*Temperature*").lineplot(
hue="variable", style="climate_model", alpha=0.7, linewidth=2, ax=ax
)
ax.legend(loc="upper left")
ax = fig.add_subplot(122)
combined.filter(variable="Heat Uptake").lineplot(
hue="climate_model", style="climate_model", alpha=0.7, linewidth=2, ax=ax
)
[11]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

Appendix A¶
We begin with the definitions of the \(a\) constants,
We then have
Recalling the definition of the \(\phi\) parameters,
We have,
Recalling the definition of the \(\tau\) parameters,
We have,
Recalling the definition of the \(b\) parameters,
We then have
Putting it all back together,
One layer model¶
Here we show how to run our two-layer model as a single-layer model. There are two different ways to do this, which we present below.
Imports and loading data¶
[1]:
# NBVAL_IGNORE_OUTPUT
import os.path
import numpy as np
import pandas as pd
from openscm_units import unit_registry as ur
import tqdm.autonotebook as tqdman
from scmdata import ScmRun, run_append
from openscm_twolayermodel import TwoLayerModel
import matplotlib.pyplot as plt
/home/docs/checkouts/readthedocs.org/user_builds/openscm-two-layer-model/envs/stable/lib/python3.7/site-packages/ipykernel_launcher.py:7: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
import sys
For this we use an idealised scenario which is a reasonable representation of the forcing which occurs in response to an abrupt doubling in atmospheric CO\(_2\) concentrations (often referred to as an abrupt-2xCO2 experiment).
[2]:
run_length = 2000
data = np.zeros(run_length)
data[10 : ] = 4.0
driver = ScmRun(
data=data,
index=1850 + np.arange(run_length),
columns={
"unit": "W/m^2",
"model": "idealised",
"scenario": "1pctCO2",
"region": "World",
"variable": "Effective Radiative Forcing",
},
)
driver
[2]:
<scmdata.ScmRun (timeseries: 1, timepoints: 2000)>
Time:
Start: 1850-01-01T00:00:00
End: 3849-01-01T00:00:00
Meta:
model region scenario unit variable
0 idealised World 1pctCO2 W/m^2 Effective Radiative Forcing
[3]:
# NBVAL_IGNORE_OUTPUT
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111)
driver.filter(variable="Effective Radiative Forcing").lineplot()
[3]:
<AxesSubplot:xlabel='time', ylabel='W/m^2'>

No second layer¶
The first, and arguably the simplest way, to make a single layer model is to simply remove the connection between the top and second layers. Recalling the equations which define the two-layer model below,
We see that we can effectively remove the second layer by setting \(\eta = 0\).
[4]:
TwoLayerModel().eta
[4]:
[5]:
# NBVAL_IGNORE_OUTPUT
eta_values = np.array([0, 0.8]) * ur("W/m^2/K")
eta_values
[5]:
Magnitude | [0.0 0.8] |
---|---|
Units | watt/(kelvin meter2) |
[6]:
# NBVAL_IGNORE_OUTPUT
du_values = np.array([50, 250, 500]) * ur("m")
du_values
[6]:
Magnitude | [ 50 250 500] |
---|---|
Units | meter |
[7]:
# NBVAL_IGNORE_OUTPUT
runner = TwoLayerModel()
output = []
equivalent_parameters = []
for eta in tqdman.tqdm(eta_values, desc="eta values", leave=False):
runner.eta = eta
for du in tqdman.tqdm(du_values, desc="du values", leave=False):
runner.du = du
output.append(runner.run_scenarios(driver))
output = run_append(output)
output.head()
[7]:
time | 1850-01-01 00:00:00 | 1851-01-01 00:00:00 | 1852-01-01 00:00:00 | 1853-01-01 00:00:00 | 1854-01-01 00:00:00 | 1855-01-01 00:00:00 | 1856-01-01 00:00:00 | 1857-01-01 00:00:00 | 1858-01-01 00:00:00 | 1859-01-01 00:00:00 | ... | 3840-01-01 00:00:00 | 3841-01-01 00:00:00 | 3842-01-01 00:00:00 | 3843-01-01 00:00:00 | 3844-01-01 00:00:00 | 3845-01-01 00:00:00 | 3846-01-01 00:00:00 | 3847-01-01 00:00:00 | 3848-01-01 00:00:00 | 3849-01-01 00:00:00 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a (watt / delta_degree_Celsius ** 2 / meter ** 2) | climate_model | dl (meter) | du (meter) | efficacy (dimensionless) | eta (watt / kelvin / meter ** 2) | lambda0 (watt / delta_degree_Celsius / meter ** 2) | model | region | run_idx | scenario | unit | variable | |||||||||||||||||||||
0.0 | two_layer | 1200 | 50 | 1.0 | 0.0 | 1.246667 | idealised | World | 0 | 1pctCO2 | W/m^2 | Effective Radiative Forcing | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 |
delta_degC | Surface Temperature|Upper | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 3.208556 | 3.208556 | 3.208556 | 3.208556 | 3.208556 | 3.208556 | 3.208556 | 3.208556 | 3.208556 | 3.208556 | |||||||||||
Surface Temperature|Lower | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ||||||||||||
W/m^2 | Heat Uptake | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | |||||||||||
250 | 1.0 | 0.0 | 1.246667 | idealised | World | 0 | 1pctCO2 | W/m^2 | Effective Radiative Forcing | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 |
5 rows × 2000 columns
As we can see in the plots below, the runs with \(\eta = 0\) only have a single timescale in their response. In contrast, the runs with \(\eta \neq 0\) have two clear, distinct timescales. Notably, because equilibrium warming is independent of ocean heat uptake, the equilibrium warming is the same in all cases.
As expected, we see that the depth of the mixed-layer affects the response time of the mixed-layer (the only response time in the case of \(\eta = 0\)) whilst having a much smaller effect on the response time of the deep ocean.
[8]:
# NBVAL_IGNORE_OUTPUT
scenario_to_plot = "1pctCO2"
xlim = [1850, 3500]
pkwargs = dict(
hue="du (meter)",
style="eta (watt / kelvin / meter ** 2)",
time_axis="year"
)
fig = plt.figure(figsize=(9, 9))
ax = fig.add_subplot(211)
output.filter(scenario=scenario_to_plot, variable="Surface Temperature|Upper").lineplot(**pkwargs, ax=ax)
ax.set_title("Surface Temperature|Upper")
ax = fig.add_subplot(212, sharex=ax)
output.filter(scenario=scenario_to_plot, variable="Heat Uptake").lineplot(**pkwargs, ax=ax)
ax.set_title("Heat Uptake")
ax.set_xlim(xlim)
plt.tight_layout()

Infinite reservoir second layer¶
If we make the deep ocean component of the two-layer model infinitely deep, then we also have a single layer model. The concept is described by Equation 4 of Geoffroy et al. 2013, Part 1.
In short, if \(C_D \rightarrow \infty\), then \(T_D = 0\) and the equation governing the mixed layer response becomes
In effect, we alter the climate feedback factor from \(\lambda_0 - a T\) to \(\lambda_0 - a T + \epsilon \eta\) we increase the climate feedback factor and hence lower the equilibrium climate sensitivity.
[9]:
# NBVAL_IGNORE_OUTPUT
dl_values = np.array([10 ** 3, 10 ** 4, 10 ** 5, 10 ** 15]) * ur("m")
dl_values
[9]:
Magnitude | [ 1000 10000 100000 1000000000000000] |
---|---|
Units | meter |
[10]:
# NBVAL_IGNORE_OUTPUT
runner = TwoLayerModel()
output = []
equivalent_parameters = []
for dl in tqdman.tqdm(dl_values, desc="dl values", leave=False):
runner.dl = dl
output.append(runner.run_scenarios(driver))
equivalent_parameters.append(({"two-layer deep ocean depth": runner.dl}, runner.get_impulse_response_parameters()))
output = run_append(output)
output.head()
[10]:
time | 1850-01-01 00:00:00 | 1851-01-01 00:00:00 | 1852-01-01 00:00:00 | 1853-01-01 00:00:00 | 1854-01-01 00:00:00 | 1855-01-01 00:00:00 | 1856-01-01 00:00:00 | 1857-01-01 00:00:00 | 1858-01-01 00:00:00 | 1859-01-01 00:00:00 | ... | 3840-01-01 00:00:00 | 3841-01-01 00:00:00 | 3842-01-01 00:00:00 | 3843-01-01 00:00:00 | 3844-01-01 00:00:00 | 3845-01-01 00:00:00 | 3846-01-01 00:00:00 | 3847-01-01 00:00:00 | 3848-01-01 00:00:00 | 3849-01-01 00:00:00 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a (watt / delta_degree_Celsius ** 2 / meter ** 2) | climate_model | dl (meter) | du (meter) | efficacy (dimensionless) | eta (watt / delta_degree_Celsius / meter ** 2) | lambda0 (watt / delta_degree_Celsius / meter ** 2) | model | region | run_idx | scenario | unit | variable | |||||||||||||||||||||
0.0 | two_layer | 1000 | 50 | 1.0 | 0.8 | 1.246667 | idealised | World | 0 | 1pctCO2 | W/m^2 | Effective Radiative Forcing | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 |
delta_degC | Surface Temperature|Upper | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 3.207635 | 3.207638 | 3.207642 | 3.207645 | 3.207648 | 3.207652 | 3.207655 | 3.207658 | 3.207661 | 3.207665 | |||||||||||
Surface Temperature|Lower | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 3.206227 | 3.206236 | 3.206244 | 3.206252 | 3.206261 | 3.206269 | 3.206278 | 3.206286 | 3.206294 | 3.206302 | ||||||||||||
W/m^2 | Heat Uptake | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.001153 | 0.001149 | 0.001144 | 0.001140 | 0.001136 | 0.001132 | 0.001128 | 0.001124 | 0.001120 | 0.001115 | |||||||||||
10000 | 50 | 1.0 | 0.8 | 1.246667 | idealised | World | 0 | 1pctCO2 | W/m^2 | Effective Radiative Forcing | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 | 4.000000 |
5 rows × 2000 columns
As we can see below, as the deep ocean becomes deeper and deeper, its equivalent timescale increases. This demonstrates that the deep ocean is becoming increasingly close to being an infinite reservoir.
[11]:
for v in equivalent_parameters:
v[1]["d1"] = v[1]["d1"].to("yr")
v[1]["d2"] = v[1]["d2"].to("yr")
equivalent_parameters
[11]:
[({'two-layer deep ocean depth': 1000 <Unit('meter')>},
{'d1': 3.211845269334279 <Unit('a')>,
'd2': 273.9854219906419 <Unit('a')>,
'q1': 0.4810875417166762 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'q2': 0.32105149571648217 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'efficacy': 1.0 <Unit('dimensionless')>}),
({'two-layer deep ocean depth': 10000 <Unit('meter')>},
{'d1': 3.2342013046041056 <Unit('a')>,
'd2': 2720.915300585768 <Unit('a')>,
'q1': 0.4878523568580023 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'q2': 0.3142866805751838 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'efficacy': 1.0 <Unit('dimensionless')>}),
({'two-layer deep ocean depth': 100000 <Unit('meter')>},
{'d1': 3.2364276918618766 <Unit('a')>,
'd2': 27190.435420502465 <Unit('a')>,
'q1': 0.4885246922441889 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'q2': 0.31361434518885845 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'efficacy': 1.0 <Unit('dimensionless')>}),
({'two-layer deep ocean depth': 1000000000000000 <Unit('meter')>},
{'d1': 3.238959349323281 <Unit('a')>,
'd2': 271883581625601.66 <Unit('a')>,
'q1': 0.4889441930744013 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'q2': 0.31555972744518723 <Unit('delta_degree_Celsius * meter ** 2 / watt')>,
'efficacy': 1.0 <Unit('dimensionless')>})]
As shown in the plots below, as the deep ocean becomes bigger, it can uptake more heat and hence mixed-layer warming is reduced. However, whilst the deep ocean is finite, the mixed-layer warming does eventually reach the same equilibrium (independent of deep ocean depth), it just takes longer to do so. Once the deep ocean becomes infinite, as discussed above, we effectively have a single-layer model with an increased climate feedback factor (lower equilibrium climate sensitivity) which can uptake heat forever.
[12]:
# NBVAL_IGNORE_OUTPUT
scenario_to_plot = "1pctCO2"
xlim = [1850, 3500]
pkwargs = dict(
hue="dl (meter)",
style="variable",
time_axis="year"
)
fig = plt.figure(figsize=(9, 9))
ax = fig.add_subplot(211)
output.filter(scenario=scenario_to_plot, variable="Surface Temperature|Upper").lineplot(**pkwargs, ax=ax)
ax = fig.add_subplot(212, sharex=ax)
output.filter(scenario=scenario_to_plot, variable="Heat Uptake").lineplot(**pkwargs, ax=ax)
ax.set_xlim(xlim)
[12]:
(1850.0, 3500.0)

Development¶
If you’re interested in contributing to OpenSCM Two Layer Model, we’d love to have you on board! This section of the docs details how to get setup to contribute and how best to communicate.
Contributing¶
All contributions are welcome, some possible suggestions include:
tutorials (or support questions which, once solved, result in a new tutorial :D)
blog posts
improving the documentation
bug reports
feature requests
pull requests
Please report issues or discuss feature requests in the OpenSCM Two Layer Model issue tracker. If your issue is a feature request or a bug, please use the templates available, otherwise, simply open a normal issue.
As a contributor, please follow a couple of conventions:
Create issues in the OpenSCM Two Layer Model issue tracker for changes and enhancements, this ensures that everyone in the community has a chance to comment
Be welcoming to newcomers and encourage diverse new contributors from all backgrounds: see the Python Community Code of Conduct
Only push to your own branches, this allows people to force push to their own branches as they need without fear or causing others headaches
Start all pull requests as draft pull requests and only mark them as ready for review once they’ve been rebased onto master, this makes it much simpler for reviewers
Try and make lots of small pull requests, this makes it easier for reviewers and faster for everyone as review time grows exponentially with the number of lines in a pull request
Getting setup¶
To get setup as a developer, we recommend the following steps (if any of these tools are unfamiliar, please see the resources we recommend in Development tools):
Install conda and make
Run
make virtual-environment
, if that fails you can try doing it manuallyChange your current directory to OpenSCM Two Layer Model’s root directory (i.e. the one which contains
README.rst
),cd openscm-twolayermodel
Create a virtual environment to use with OpenSCM Two Layer Model
python3 -m venv venv
Activate your virtual environment
source ./venv/bin/activate
Upgrade pip
pip install --upgrade pip
Install the development dependencies (very important, make sure your virtual environment is active before doing this)
pip install -e .[dev]
Make sure the tests pass by running
make checks
, if that fails the commands can be read out of theMakefile
Getting help¶
Whilst developing, unexpected things can go wrong (that’s why it’s called ‘developing’, if we knew what we were doing, it would already be ‘developed’). Normally, the fastest way to solve an issue is to contact us via the issue tracker. The other option is to debug yourself. For this purpose, we provide a list of the tools we use during our development as starting points for your search to find what has gone wrong.
Development tools¶
This list of development tools is what we rely on to develop OpenSCM Two Layer Model reliably and reproducibly. It gives you a few starting points in case things do go inexplicably wrong and you want to work out why. We include links with each of these tools to starting points that we think are useful, in case you want to learn more.
-
we use a blend of pytest and the inbuilt Python testing capabilities for our tests so checkout what we’ve already done in
tests
to get a feel for how it works
Continuous integration (CI) (also brief intro blog post and a longer read)
we use GitHub CI for our CI but there are a number of good providers
-
Jupyter is automatically included in your virtual environment if you follow our Getting setup instructions
Other tools¶
We also use some other tools which aren’t necessarily the most familiar. Here we provide a list of these along with useful resources.
-
we use regex101.com to help us write and check our regular expressions, make sure the language is set to Python to make your life easy!
Formatting¶
To help us focus on what the code does, not how it looks, we use a couple of automatic formatting tools.
These automatically format the code for us and tell use where the errors are.
To use them, after setting yourself up (see Getting setup), simply run make format
.
Note that make format
can only be run if you have committed all your work i.e. your working directory is ‘clean’.
This restriction is made to ensure that you don’t format code without being able to undo it, just in case something goes wrong.
Buiding the docs¶
After setting yourself up (see Getting setup), building the docs is as simple as running make docs
(note, run make -B docs
to force the docs to rebuild and ignore make when it says ‘… index.html is up to date’).
This will build the docs for you.
You can preview them by opening docs/build/html/index.html
in a browser.
For documentation we use Sphinx. To get ourselves started with Sphinx, we started with this example then used Sphinx’s getting started guide.
Gotchas¶
To get Sphinx to generate pdfs (rarely worth the hassle), you require Latexmk.
On a Mac this can be installed with sudo tlmgr install latexmk
.
You will most likely also need to install some other packages (if you don’t have the full distribution).
You can check which package contains any missing files with tlmgr search --global --file [filename]
.
You can then install the packages with sudo tlmgr install [package]
.
Docstring style¶
For our docstrings we use numpy style docstrings. For more information on these, here is the full guide and the quick reference we also use.
Releasing¶
First step¶
Test installation with dependencies
make test-install
Update
CHANGELOG.rst
add a header for the new version between
master
and the latest bullet pointthis should leave the section underneath the master header empty
git add .
git commit -m "Prepare for release of vX.Y.Z"
git tag vX.Y.Z
Test version updated as intended with
make test-install
Push to repository¶
To do the release, push the tags and the repository state.
git push
git push --tags
Assuming all the checks pass, this automatically triggers a release on PyPI via the .github/workflows/ci-cd-workflow.yml
action.
Why is there a Makefile
in a pure Python repository?¶
Whilst it may not be standard practice, a Makefile
is a simple way to automate general setup (environment setup in particular).
Hence we have one here which basically acts as a notes file for how to do all those little jobs which we often forget e.g. setting up environments, running tests (and making sure we’re in the right environment), building docs, setting up auxillary bits and pieces.
Base API¶
Module containing the base for model implementations
- class openscm_twolayermodel.base.Model¶
Bases:
abc.ABC
Base class for model implementations
- reset()¶
Reset everything so that a new run can be performed.
Called as late as possible before
run()
.
- run()¶
Run the model.
- abstract set_drivers(*args, **kwargs)¶
Set the model’s drivers
- step()¶
Do a single time step.
- class openscm_twolayermodel.base.TwoLayerVariant¶
Bases:
openscm_twolayermodel.base.Model
Base for variations of implementations of the two-layer model
- property delta_t¶
pint.Quantity
Time step for forward-differencing approximation
- property erf¶
pint.Quantity
Effective radiative forcing
- reset()¶
Reset everything so that a new run can be performed.
Called as late as possible before
run()
.
- run()¶
Run the model.
- run_scenarios(scenarios, driver_var='Effective Radiative Forcing', progress=True)¶
Run scenarios.
The model timestep is automatically adjusted based on the timestep used in
scenarios
. The timestep used inscenarios
must be constant because this implementation has a constant timestep. Pull requests to upgrade the implementation to support variable timesteps are welcome https://github.com/openscm/openscm-twolayermodel/pulls.- Parameters
scenarios (
ScmDataFrame
orScmRun
orpyam.IamDataFrame
orpd.DataFrame
ornp.ndarray
or str) – Scenarios to run. The input will be converted to anScmRun
before the run takes place.driver_var (str) – The variable in
scenarios
to use as the driver of the modelprogress (bool) – Whether to display a progress bar
- Returns
Results of the run (including drivers)
- Return type
ScmRun
- Raises
ValueError – No data is available for
driver_var
in the"World"
region inscenarios
.
- set_drivers(erf)¶
Set drivers for a model run
- Parameters
erf (
pint.Quantity
) – Effective radiative forcing (W/m^2) to use to drive the model- Raises
AssertionError –
erf
is not one-dimensional
- step()¶
Do a single time step.
Impulse Response Model API¶
Module containing the impulse response model
The 2-timescale impulse response model is mathematically equivalent to the two-layer model without state dependence.
- class openscm_twolayermodel.impulse_response_model.ImpulseResponseModel(q1=<Quantity(0.3, 'delta_degree_Celsius * meter ** 2 / watt')>, q2=<Quantity(0.4, 'delta_degree_Celsius * meter ** 2 / watt')>, d1=<Quantity(9.0, 'a')>, d2=<Quantity(400.0, 'a')>, efficacy=<Quantity(1.0, 'dimensionless')>, delta_t=<Quantity(0.0833333333, 'a')>)¶
Bases:
openscm_twolayermodel.base.TwoLayerVariant
TODO: top line and paper references
This implementation uses a forward-differencing approach. This means that temperature and ocean heat uptake values are start of timestep values. For example, temperature[i] is only affected by drivers from the i-1 timestep. In practice, this means that the first temperature and ocean heat uptake values will always be zero and the last value in the input drivers has no effect on model output.
- property d1¶
pint.Quantity
Response timescale of first box
- property d2¶
pint.Quantity
Response timescale of second box
- property delta_t¶
pint.Quantity
Time step for forward-differencing approximation
- property efficacy¶
pint.Quantity
Efficacy factor
- property erf¶
pint.Quantity
Effective radiative forcing
- get_two_layer_parameters()¶
Get equivalent two-layer model parameters
For details on how the equivalence is calculated, please see the notebook
impulse-response-equivalence.ipynb
in the OpenSCM Two Layer model repository.- Returns
dict of str – Input arguments to initialise an
openscm_twolayermodel.TwoLayerModel
with the same temperature response asself
- Return type
pint.Quantity
- property q1¶
pint.Quantity
Sensitivity of first box response to radiative forcing
- property q2¶
pint.Quantity
Sensitivity of second box response to radiative forcing
- reset()¶
Reset everything so that a new run can be performed.
Called as late as possible before
run()
.
- run()¶
Run the model.
- run_scenarios(scenarios, driver_var='Effective Radiative Forcing', progress=True)¶
Run scenarios.
The model timestep is automatically adjusted based on the timestep used in
scenarios
. The timestep used inscenarios
must be constant because this implementation has a constant timestep. Pull requests to upgrade the implementation to support variable timesteps are welcome https://github.com/openscm/openscm-twolayermodel/pulls.- Parameters
scenarios (
ScmDataFrame
orScmRun
orpyam.IamDataFrame
orpd.DataFrame
ornp.ndarray
or str) – Scenarios to run. The input will be converted to anScmRun
before the run takes place.driver_var (str) – The variable in
scenarios
to use as the driver of the modelprogress (bool) – Whether to display a progress bar
- Returns
Results of the run (including drivers)
- Return type
ScmRun
- Raises
ValueError – No data is available for
driver_var
in the"World"
region inscenarios
.
- set_drivers(erf)¶
Set drivers for a model run
- Parameters
erf (
pint.Quantity
) – Effective radiative forcing (W/m^2) to use to drive the model- Raises
AssertionError –
erf
is not one-dimensional
- step()¶
Do a single time step.
Two Layer Model API¶
Module containing the two-layer model
- class openscm_twolayermodel.two_layer_model.TwoLayerModel(du=<Quantity(50, 'meter')>, dl=<Quantity(1200, 'meter')>, lambda0=<Quantity(1.24666667, 'watt / delta_degree_Celsius / meter ** 2')>, a=<Quantity(0.0, 'watt / delta_degree_Celsius ** 2 / meter ** 2')>, efficacy=<Quantity(1.0, 'dimensionless')>, eta=<Quantity(0.8, 'watt / delta_degree_Celsius / meter ** 2')>, delta_t=<Quantity(31557600.0, 'second')>)¶
Bases:
openscm_twolayermodel.base.TwoLayerVariant
TODO: top line and paper references
This implementation uses a forward-differencing approach. This means that temperature and ocean heat uptake values are start of timestep values. For example, temperature[i] is only affected by drivers from the i-1 timestep. In practice, this means that the first temperature and ocean heat uptake values will always be zero and the last value in the input drivers has no effect on model output.
- property a¶
pint.Quantity
Dependence of climate feedback factor on temperature
- property delta_t¶
pint.Quantity
Time step for forward-differencing approximation
- property dl¶
pint.Quantity
Depth of lower layer
- property du¶
pint.Quantity
Depth of upper layer
- property efficacy¶
pint.Quantity
Efficacy factor
- property erf¶
pint.Quantity
Effective radiative forcing
- property eta¶
pint.Quantity
Heat transport efficiency
- get_impulse_response_parameters()¶
Get equivalent two-timescale impulse response model parameters
For details on how the equivalence is calculated, please see the notebook
impulse-response-equivalence.ipynb
in the OpenSCM Two Layer model repository.- Returns
dict of str – Input arguments to initialise an
openscm_twolayermodel.ImpulseResponseModel
with the same temperature response asself
- Return type
pint.Quantity
- Raises
ValueError –
self.a
is non-zero, the two-timescale model does not support state-dependence.
- property heat_capacity_lower¶
pint.Quantity
Heat capacity of lower layer
- property heat_capacity_upper¶
pint.Quantity
Heat capacity of upper layer
- property lambda0¶
pint.Quantity
Initial climate feedback factor
- reset()¶
Reset everything so that a new run can be performed.
Called as late as possible before
run()
.
- run()¶
Run the model.
- run_scenarios(scenarios, driver_var='Effective Radiative Forcing', progress=True)¶
Run scenarios.
The model timestep is automatically adjusted based on the timestep used in
scenarios
. The timestep used inscenarios
must be constant because this implementation has a constant timestep. Pull requests to upgrade the implementation to support variable timesteps are welcome https://github.com/openscm/openscm-twolayermodel/pulls.- Parameters
scenarios (
ScmDataFrame
orScmRun
orpyam.IamDataFrame
orpd.DataFrame
ornp.ndarray
or str) – Scenarios to run. The input will be converted to anScmRun
before the run takes place.driver_var (str) – The variable in
scenarios
to use as the driver of the modelprogress (bool) – Whether to display a progress bar
- Returns
Results of the run (including drivers)
- Return type
ScmRun
- Raises
ValueError – No data is available for
driver_var
in the"World"
region inscenarios
.
- set_drivers(erf)¶
Set drivers for a model run
- Parameters
erf (
pint.Quantity
) – Effective radiative forcing (W/m^2) to use to drive the model- Raises
AssertionError –
erf
is not one-dimensional
- step()¶
Do a single time step.
Constants API¶
Physical constants used in calculations
- openscm_twolayermodel.constants.DENSITY_WATER = <Quantity(1000.0, 'kilogram / meter ** 3')>¶
density of water
- Type
pint.Quantity
- openscm_twolayermodel.constants.HEAT_CAPACITY_WATER = <Quantity(4181.0, 'joule / delta_degree_Celsius / kilogram')>¶
heat capacity of water
- Type
pint.Quantity
Errors API¶
Exceptions raised within openscm_twolayermodel
- exception openscm_twolayermodel.errors.ModelStateError¶
Bases:
ValueError
Exception raised if a model’s state is incompatible with the action
- args¶
- with_traceback()¶
Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
- exception openscm_twolayermodel.errors.UnitError¶
Bases:
ValueError
Exception raised if something has the wrong units
- args¶
- with_traceback()¶
Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
Utils API¶
Utility functions
- openscm_twolayermodel.utils.convert_lambda_to_ecs(lambda_val, f2x=<Quantity(3.74, 'watt / meter ** 2')>)¶
Convert a lambda value to equilibrium climate sensitivity (ECS)
- Parameters
lambda_val (
pint.Quantity
) – Value of lambda to convert to ECSf2x (
pint.Quantity
) – Value of the forcing due to a doubling of atmospheric CO2 to assume during the conversion
- Returns
ECS value
- Return type
pint.Quantity
- Raises
TypeError –
lambda_val
orf2x
is not apint.Quantity
.
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
The changes listed in this file are categorised as follows:
Added: new features
Changed: changes in existing functionality
Deprecated: soon-to-be removed features
Removed: now removed features
Fixed: any bug fixes
Security: in case of vulnerabilities.
v0.2.1 - 2020-12-23¶
Added¶
(#20) Statement of need to the README following JOSS review (closes #18)
Changed¶
(#26) Updated to new scmdata version (and hence new openscm-units API)
(#25) JOSS paper following JOSS review 1
(#23) Moved notebooks into full documentation following JOSS review (closes #17)
(#21) Quoted pip install instructions to ensure cross-shell compatibility following JOSS review (closes #16)
(#20) Option to remove tqdm progress bar by passing
progress=False