Basic example
FaIR v2.2 is object-oriented and designed to be more flexible than its
predecessors. This does mean that setting up a problem is different to
before - gone are the days of 60 keyword arguments to fair_scm
and
we now use classes and functions with fewer arguments that in the long
run should be easier to use. Of course, there is a learning curve, and
will take some getting used to. This tutorial aims to walk through a
simple problem using FaIR 2.2.
The structure of FaIR 2.2 centres around the FAIR
class, which
contains all information about the scenario(s), the forcer(s) we want to
investigate, and any configurations specific to each species and the
response of the climate.
Note
The code in this introductory block is explanatory and if you try to
copy and paste it it you’ll get errors. The code in this file is
self-contained below the heading “1. Create FaIR instance” below.
Alternatively, check out the repository from GitHub and run this example
notebook in jupyter
. Details
here.
Some basics
There are two main parts to running fair. The first is setting up the problem definition. This tells fair what you’re including in the run, how long you are running for, how many scenarios and how many (climate) ensemble members (known as configs).
Setting up the run
First make a new instance:
from fair import FAIR
f = FAIR()
Then, we need to add some information about the time horizon of our model, forcers we want to run with, their configuration (and the configuration of the climate), and optionally some model control options:
from fair.io import read_properties
f.define_time(2000, 2050, 1)
f.define_scenarios(['abrupt', 'ramp'])
f.define_configs(['high', 'central', 'low'])
species, properties = read_properties()
f.define_species(species, properties)
f.ghg_method='Myhre1998'
Finally, we tell fair to generate some empty (all NaN) array variables: emissions, concentrations, forcing, temperature etc.:
f.allocate()
That’s all you need to set up the run.
Fill in data
The f.allocate()
step creates xarray
DataArrays, some of which
need to be populated. For example, to fill a constant 40 GtCO2/yr
emissions rate for fossil CO2 emissions into the ‘abrupt’ scenario, we
can do
from fair.interface import fill
fill(f.emissions, 40, scenario='abrupt', specie='CO2 FFI')
...
In this section we also want to tell fair things such as the climate feedback parameter, carbon cycle sensitivities, aerosol forcing parameters, and literally hundreds of other things you can vary. There are convenience functions for reading in emissions and parameter sets from external files.
Run and analyse model
Finally, the model is run with
f.run()
Results are stored within the FAIR
instance as xarray
DataArrays
or Dataset, and can be obtained such as
print(fair.temperature)
Multiple scenarios
and configs
can be supplied in a FAIR
instance, and due to internal parallelisation is the fastest way to run
the model (100 ensemble members per second for 1750-2100 on my Mac for
an emissions driven run). The total number of scenarios that will be run
is the product of scenarios
and configs
. For example we might
want to run three emissions scenarios
– let’s say SSP1-2.6, SSP2-4.5
and SSP3-7.0 – using climate calibrations (configs
) from the UKESM,
GFDL, MIROC and NorESM climate models. This would give us a total of 12
ensemble members in total which are run in parallel.
Recommended order for setting up a problem
In this tutorial the recommended order in which to define a problem is set out step by step, and is as follows:
Create the
FAIR
instance, inititalised with run control options.Define the time horizon of the problem with
FAIR.define_time()
Define the scenarios to be run (e.g. SSPs, IAM emissions scenarios, or anything you want) with
FAIR.define_scenarios()
.Define the configurations to be run with
FAIR.define_configs()
. A configuration (config
) is a set of parameters that describe climate response and species response parameters. For example you might have aconfig
with high climate sensitivity and strong aerosol forcing, and one with low climate sensitivity and weak aerosol forcing.Define which
specie
s will be included in the problem, and their properties including the run mode (e.g. emissions-driven, concentration driven) withFAIR.define_species()
.Optionally, modify run control options.
Create input and output
DataArrays
withFAIR.allocate()
.Fill in the DataArrays (e.g. emissions), climate configs, and species configs, by either working directly with the
xarray
API, or using FaIR-packaged convenience functions likefill
,initialise
,f.fill_from_csv
andf.override_defaults
.Run:
FAIR.run()
.Analyse results by accessing the DataArrays that are attributes of
FAIR
.
1. Create FaIR instance
We’ll call our instance f
: it’s nice and short and the fair
name
is reserved for the module.
from fair import FAIR
f = FAIR()
2. Define time horizon
There are two different time indicators in FaIR: the timebound
and
the timepoint
. timebound
s, as the name suggests, are at the
edges of each time step; they can be thought of as instantaneous
snapshots. timepoint
s are what happens between time bounds and are
rates or integral quantities.
The main thing to remember is that only emissions
are defined on
timepoint
s and everything else is defined on timebound
s, and
when we specify the time horizon in our model, we are defining the
timebound
s of the problem.
Secondly, the number of timebound
s is one more than the number of
timepoint
s, as the start and end points are included in the
timebound
s.
# create time horizon with bounds of 2000 and 2050, at 1-year intervals
f.define_time(2000, 2050, 1)
print(f.timebounds)
print(f.timepoints)
3. Define scenarios
The scenarios are a list of strings that label the scenario dimension of the model, helping you keep track of inputs and outputs.
In this example problem we will create two scenarios: an “abrupt” scenario (where emissions or concentrations change instantly) and a “ramp” scenario where they change gradually.
# Define two scenarios
f.define_scenarios(["abrupt", "ramp"])
f.scenarios
4. Define configs
Similarly to the scenarios, the configs are a labelling tool. Each config has associated climate- and species-related settings, which we will come to later.
We’ll use three config sets, crudely corresponding to high, medium and low climate sensitivity.
# Define three scenarios
f.define_configs(["high", "central", "low"])
f.configs
5. Define species
This defines the forcers – anthropogenic or natural – that are present
in your scenario. A species
could be something directly emitted like
CO2 from fossil fuels, or it could be a category where forcing has to be
calculate from precursor emissions like aerosol-cloud interactions.
Each specie
is assigned a name that is used to distinguish it from
other species. You can call the species what you like within the model
as long as you are consistent. We also pass a dictionary of
properties
that defines how each specie behaves in the model.
In this example we’ll start off running a scenario with CO2 from fossil fuels and industry, CO2 from AFOLU, CH4, N2O, and Sulfur, and Volcanic forcing (note you don’t need the full 40 species used in v1.1-1.6, and some additional default ones are included). From these inputs we also want to determine forcing from aerosol-radiation and aerosol-cloud interactions, as well as CO2, CH4 and N2O.
To highlight some of the functionality we’ll run CO2 and Sulfur
emissions-driven, and CH4 and N2O concentration-driven. (This is akin to
an esm-ssp585
kind of run from CMIP6, though with fewer species).
We’ll use totally fake data here - this is not intended to represent a
real-world scenario but just to highlight how FaIR works.
Full simulations may have 50 or more species included and the
properties
dictionary can get quite large, so it can be beneficial
to edit it in a CSV and load it in. This is what is done here - we have
taken the default species_configs_properties
file and cut it down to
keep only the species we care about.
Note the label you have given each specie must appear in the first column of this file.
from fair.io import read_properties
species, properties = read_properties('data/importing-data/species_configs_properties.csv')
f.define_species(species, properties)
In total, we have 9 species in this model. We want to run
CO2 fossil and industry
CO2 AFOLU
Sulfur
with specified emissions.
We want to run
CH4
N2O
with specified concentrations. We want to include an external time series from
Volcanic
We also want to calculate forcing from CO2, so we need to declare the CO2 as a greenhouse gas in addition to its emitted components:
CO2
and we want to calculate forcing from aerosol radiation and aerosol cloud interactions
ERFari
ERFaci
Let’s examine them:
f.species
f.properties
properties
is just a dictionary and species
just a list. You can
define them from scratch, though as you can see even with nine species
the dictionary gets quite long so it is easier to read it in. In the
properties
dictionary, the keys must match the species
that you
have declared.
The properties
dictionary contains five keys:
type
defines the species type such as CO2, an aerosol precursor, or volcanic forcing; there’s around 20 pre-defined types in FaIR, and thetype
defines several hard-coded properties about what the specie does. Some can only be defined once per scenario, some can have multiple species attached to its type (e.g.f-gas
). See the cell below for a list.input_mode
: how the model should be driven with thisspecie
. Valid values areemissions
,concentration
,forcing
orcalculated
and not all options are valid for alltype
s (e.g. running solar forcing with concentrations).calculated
means that the emissions/concentration/forcing of this specie depends on others, for example aerosol radiative forcing needs precursors to be emitted.greenhouse_gas
: True if thespecie
is a greenhouse gas, which means that an associatedconcentration
can be calculated (along with some other species-specific behaviours). Note that CO2 emissions from fossil fuels or from AFOLU are not treated as greenhouse gases.aerosol_chemistry_from_emissions
: Some routines such as aerosols, methane lifetime, or ozone forcing, relate to emissions of short-lived climate forcers. If thisspecie
is one of these, this should be set to True.aerosol_chemistry_from_concentration
: As above, but if the production of ozone, aerosol etc. depends on the concentration of a greenhouse gas.
# Here's a list of the species types: this cell not necessary for running fair
import pandas as pd
from fair.structure.species import species_types, valid_input_modes, multiple_allowed
types = pd.DataFrame(
{
'type': species_types,
'valid_input_modes': [valid_input_modes[specie] for specie in species_types],
'multiple_allowed': [multiple_allowed[specie] for specie in species_types]
}
)
types.set_index('type', inplace=True)
types
6. Modify run options
When we initialise the FAIR class, a number of options are given as defaults.
Let’s say we want to change the greenhouse gas forcing treatment from Meinshausen et al. 2020 default to Myhre et al. 1998. While this could have been done when initialising the class, we can also do it by setting the appropriate attribute.
help(f)
f.ghg_method
f.ghg_method='myhre1998'
f.ghg_method
7. Create input and output data
Steps 2–5 above dimensioned our problem; now, we want to actually create some data to put into it.
First we allocate the data arrays with
f.allocate()
This has created our arrays with the correct dimensions as attributes of
the FAIR
class:
f.emissions
f.temperature
8. Fill in the data
The data created is nothing more special than xarray
DataArrays.
8a. fill emissions, concentrations …
Using xarray
methods we can allocate values to the emissions. For
example, to fill CO2 fossil emissions in the abrupt scenario with a
constant emissions rate of 38 GtCO2/yr (about present-day levels), do:
f.emissions.loc[(dict(specie="CO2 FFI", scenario="abrupt"))] = 38
To do this for every specie, especially if you want to create time-varying scenarios and dimension the scenarios correctly, is very fiddly and time consuming. Therefore, we have a way to read in scenarios from CSV files:
f.fill_from_csv(
emissions_file='data/basic_run_example/emissions.csv',
concentration_file='data/basic_run_example/concentration.csv',
forcing_file='data/basic_run_example/forcing.csv'
)
Let’s have a look at what we’re reading in:
pd.read_csv('data/basic_run_example/emissions.csv')
pd.read_csv('data/basic_run_example/concentration.csv')
pd.read_csv('data/basic_run_example/forcing.csv')
The csv reader will also interpolate, so you don’t have to specify every year in your scenario. We have separate functions for reading in files from the Reduced Complexity Model Intercomparison Project (the SSPs).
Note also we haven’t filled in every species. CO2 is calculated
(the
sum of CO2 FFI
and CO2 AFOLU
), which will correctly determine
emissions, concentrations and forcing. Aerosol-radiation interactions
and Aerosol-cloud interactions are also calculated
, from emissions
of Sulfur. If the properties
, particularly type
and
input_mode
are correctly specified for each specie, fair
knows
what to do with your data.
The other thing that we have to do is define the initial conditions of
our data. If you forget to do this, you might get NaN value errors in
fair
; this is deliberate, we want the user to think about how they
engage with the model!
Using non-zero initial conditions can be useful for “restart runs”: switching from concentration-driven to emissions-driven (ZECMIP); running constant forcing commitments; running interative/adaptive emissions scenarios; the possibilities are endless.
As CO2 is emissions-driven, we also need to define its starting concentration.
Again, we have a convenience function that can handle some of the heavy lifting:
# Define initial conditions
from fair.interface import initialise
initialise(f.concentration, 278.3, specie='CO2')
initialise(f.forcing, 0)
initialise(f.temperature, 0)
initialise(f.cumulative_emissions, 0)
initialise(f.airborne_emissions, 0)
initialise(f.ocean_heat_content_change, 0)
8b. Fill in configs
This defines how the forcing is calculated, and how the model responds to a forcing.
There are two xarray.Dataset
s in fair
that define the
behaviour of the model, which are f.climate_configs
and
f.species_configs
.
Many, many species_configs
parameters have sensible defaults that
would make little impact, or little sense, to change. We can add these
parameters to the species_configs_properties
file that we read in
earlier that would autopopulate most fields. Let’s do this.
f.fill_species_configs('data/importing-data/species_configs_properties.csv')
f.species_configs
However, there’s no fun if they are all the same for each config - we
want to use fair to sample an ensemble. Again we can read these in, with
columns following a naming convention. This also fills the
climate_configs
, which are NaN by default (again we don’t want
people running things without thinking).
f.override_defaults('data/basic_run_example/configs_ensemble.csv')
and this is how it looks
pd.read_csv('data/basic_run_example/configs_ensemble.csv', index_col=0)
9. run FaIR
f.run()
10. plot results
import matplotlib.pyplot as pl
pl.plot(f.timebounds, f.temperature.loc[dict(scenario='ramp', layer=0)], label=f.configs)
pl.title('Ramp scenario: temperature')
pl.xlabel('year')
pl.ylabel('Temperature anomaly (K)')
pl.legend()
pl.plot(f.timebounds, f.concentration.loc[dict(scenario='ramp', specie='CO2')], label=f.configs)
pl.title('Ramp scenario: CO2')
pl.xlabel('year')
pl.ylabel('CO2 (ppm)')
pl.legend()
pl.plot(f.timebounds, f.forcing.loc[dict(scenario='ramp', specie='Aerosol-cloud interactions')], label=f.configs)
pl.title('Ramp scenario: forcing')
pl.xlabel('year')
pl.ylabel('ERF from aerosol-cloud interactions (W m$^{-2}$)')
pl.legend()
pl.plot(f.timebounds, f.forcing_sum.loc[dict(scenario='ramp')], label=f.configs)
pl.title('Ramp scenario: forcing')
pl.xlabel('year')
pl.ylabel('Total ERF (W m$^{-2}$)')
pl.legend()
pl.plot(f.timebounds, f.temperature.loc[dict(scenario='abrupt', layer=0)], label=f.configs)
pl.title('Abrupt scenario: temperature')
pl.xlabel('year')
pl.ylabel('Temperature anomaly (K)')
pl.legend()
pl.plot(f.timebounds, f.forcing_sum.loc[dict(scenario='abrupt')], label=f.configs)
pl.title('Abrupt scenario: forcing')
pl.xlabel('year')
pl.ylabel('Total ERF (W m$^{-2}$)')
pl.legend()
pl.plot(f.timebounds, f.concentration.loc[dict(scenario='abrupt', specie='CO2')], label=f.configs)
pl.title('Abrupt scenario: CO2')
pl.xlabel('year')
pl.ylabel('CO2 (ppm)')
pl.legend()
f.species_configs['g0'].loc[dict(specie='CO2')]
f.forcing[-1, :, 1, :]