Cumulative deployment of carbon dioxide removal (Figure 2.9)

Notebook sr15_2.3.4_carbon_dioxide_removal

This notebook is based on the Release 1.1 of the IAMC 1.5C Scenario Explorer and Data and refers to the published version of the IPCC Special Report on Global Warming of 1.5C (SR15).

The notebook is run with pyam release 0.5.0.

The source code of this notebook is available on GitHub (release 2.0.2).

sr15_2.3.4_carbon_dioxide_removal

IPCC SR15 scenario assessment

Analysis of carbon dioxide removal (CDR)

This notebook generates the assessment of carbon dioxide removal for Figure 2.9 in the IPCC's "Special Report on Global Warming of 1.5°C".

The scenario data used in this analysis can be accessed and downloaded at https://data.ene.iiasa.ac.at/iamc-1.5c-explorer.

Load pyam package and other dependencies

In [1]:
import pandas as pd
import numpy as np
import io
import itertools
import yaml
import math
import matplotlib.pyplot as plt
plt.style.use('style_sr15.mplstyle')
%matplotlib inline
import pyam
pyam - INFO: Running in a notebook, setting `pyam` logging level to `logging.INFO` and adding stderr handler

Import scenario data, categorization and specifications files

The metadata file with scenario categorisation and quantitative indicators can be downloaded at https://data.ene.iiasa.ac.at/iamc-1.5c-explorer.
Alternatively, it can be re-created using the notebook sr15_2.0_categories_indicators.

The last cell of this section loads and assigns a number of auxiliary lists as defined in the categorization notebook.

In [2]:
sr1p5 = pyam.IamDataFrame(data='../data/iamc15_scenario_data_world_r2.0.xlsx')
pyam.utils - INFO: Reading `../data/iamc15_scenario_data_world_r2.0.xlsx`
In [3]:
sr1p5.load_meta('sr15_metadata_indicators.xlsx')
pyam.core - INFO: Importing metadata for 416 scenarios (for total of 416)
In [4]:
with open("sr15_specs.yaml", 'r') as stream:
    specs = yaml.load(stream, Loader=yaml.FullLoader)

rc = pyam.run_control()
for item in specs.pop('run_control').items():
    rc.update({item[0]: item[1]})
cats = specs.pop('cats')
all_cats = specs.pop('all_cats')
subcats = specs.pop('subcats')
all_subcats = specs.pop('all_subcats')
plotting_args = specs.pop('plotting_args')
marker = specs.pop('marker')

Downselect scenario ensemble to categories of interest for this assessment

In [5]:
df = sr1p5.filter(category=cats)

Set specifications for filter and plotting

In [6]:
filter_args = dict(df=sr1p5, category=cats, marker=None, join_meta=True)

Retrieve carbon dioxide emissions and generate two auxiliary variables with net-negative CO2 emissions

For easier aggregation of the timeseries later on towards different metrics of carbon dioxide removal, we introduce both a positive net-negative timeseries (A, where the removal of 1Gt CO2 is counted as a positive value) and a timeseries where the sequestered amount is defined as a negative value (B).

In [7]:
co2 = df.filter(variable='Emissions|CO2').timeseries()

A) Net-negative CO2 emissions

In [8]:
co2_nn = co2.applymap(lambda x: - min(x, 0)).reset_index()
co2_nn.variable = 'Emissions|CO2|Net-negative'
In [9]:
co2_nn_df = pyam.IamDataFrame(co2_nn)
In [10]:
df.data = df.data.append(co2_nn_df.data, ignore_index=True)

B) Net-negative-negative CO2 emissions

In [11]:
co2_nn_neg = co2.applymap(lambda x: min(x, 0)).reset_index()
co2_nn_neg.variable = 'Emissions|CO2|Net-negative-negative'
In [12]:
co2_nn_neg_df = pyam.IamDataFrame(co2_nn_neg)
In [13]:
df.data = df.data.append(co2_nn_neg_df.data, ignore_index=True)

Retrieve carbon dioxide emissions from agriculture, forestry and land-use

In [14]:
co2_afolu = df.filter(variable='Emissions|CO2|AFOLU').timeseries()
In [15]:
co_afolu_nn = co2_afolu.applymap(lambda x: - min(x, 0)).reset_index()
co_afolu_nn.variable = 'Emissions|CO2|AFOLU|Net-negative'
In [16]:
co_afolu_nn_df = pyam.IamDataFrame(co_afolu_nn)
In [17]:
df.data = df.data.append(co_afolu_nn_df.data, ignore_index=True)

Determine emissions reductions from land use

Where possible, determine AFOLU CO2 emissions reduction relative to baseline

In [18]:
base_mapping = df.meta.reset_index()[['model', 'scenario', 'baseline']].groupby(['model'])
In [19]:
afolu_cdr_lst = []

for mapping in base_mapping:
    m = mapping[0]
    _df = co_afolu_nn_df.filter(model=m, year=range(2020, 2101))
    base_mapping_by_model = mapping[1].groupby(['baseline'])
    for _mapping in base_mapping_by_model:
        b = _mapping[0]
        base = _df.filter(scenario=b).timeseries()
        base.index = base.index.droplevel([1, 2, 3, 4])

        for s in _mapping[1].scenario:
            cdr = _df.filter(scenario=s).timeseries()
            cdr.index = cdr.index.droplevel([1, 2, 3, 4])
            cdr = cdr - base
            cdr['scenario'] = s
            afolu_cdr_lst.append(cdr)
In [20]:
afolu_cdr = pd.concat(afolu_cdr_lst, sort=False).reset_index()
afolu_cdr['region'] = 'World'
afolu_cdr['variable'] = 'Emissions|CO2|AFOLU|Net-negative reduction'
afolu_cdr['unit'] = 'MtCO2'
In [21]:
afolu_cdr_df = pyam.IamDataFrame(afolu_cdr)

For scenarios that do not provide a baseline, use the self-reported land-iuse carbon sequestration timeseries

In [22]:
alofu_cdr_direct_df = df.filter(variable='Carbon Sequestration|Land Use',
                                scenario=['PEP*', 'IMA15*', 'LowEnergyDemand'],
                                year=range(2020, 2101)
                               )

Check that methods 1 and 2 do not overlap, then merge

In [23]:
if not afolu_cdr_df.meta.index.intersection(alofu_cdr_direct_df.meta.index).empty:
    print('There is an overlap of index sets!')
In [24]:
afolu_cdr_df.data = afolu_cdr_df.data.append(alofu_cdr_direct_df.data, ignore_index=True)

Remove 'Carbon Sequestration|Land Use' from IamDataFrame and merge in alternative metrics

In [25]:
df.filter(variable='Carbon Sequestration|Land Use', keep=False, inplace=True)
In [26]:
df.data = df.data.append(afolu_cdr_df.data, ignore_index=True)
In [27]:
df.rename({'variable': {'Carbon Sequestration|Land Use': 'AFOLU CDR',
                        'Emissions|CO2|AFOLU|Net-negative reduction': 'AFOLU CDR'}}, inplace=True)
In [28]:
exclude_no_afolue_cdr = df.require_variable('AFOLU CDR', exclude_on_fail=True)
pyam.core - INFO: 77 scenarios do not include required variable `AFOLU CDR`, marked as `exclude: True` in metadata
In [29]:
df.filter(exclude=False, inplace=True)

Rename variables for plots

In [30]:
variable_mapping = [
    ('Total CDR', [
        'Carbon Sequestration|CCS|Biomass',
        'AFOLU CDR',
        'Carbon Sequestration|Direct Air Capture',
        'Carbon Sequestration|Enhanced Weathering']),
    ('AFOLU CDR', 'AFOLU CDR'),
    ('BECCS', 'Carbon Sequestration|CCS|Biomass'),
    ('Net negative CO2', 'Emissions|CO2|Net-negative'),
    ('Compensate CDR', [
        'Carbon Sequestration|CCS|Biomass',
        'AFOLU CDR',
        'Carbon Sequestration|Direct Air Capture',
        'Carbon Sequestration|Enhanced Weathering',
        'Emissions|CO2|Net-negative-negative'])
]
In [31]:
valid_variables = []
for (name, variable) in variable_mapping:
    if pyam.isstr(variable):
        valid_variables.append(variable)
    else:
        for v in variable:
            valid_variables.append(v)
In [32]:
df.filter(variable=valid_variables, inplace=True)

Plot by warming category with multiple last years

In [33]:
cats.remove('Above 2C')
In [34]:
def marker_args(m):
    return dict(zorder=4,
                edgecolors=rc['edgecolors']['marker'][m],
                c=rc['c']['marker'][m],
                marker=rc['marker']['marker'][m],
                linewidths=1)
In [35]:
def boxplot_cumulative_ccs(ymax, last_year, panel_label, legend=True):
    fig = plt.figure(figsize=(8, 3))

    _cats = len(cats) - 1
    label_list = []

    for i, (name, v) in enumerate(variable_mapping):
        _df = df.filter(variable=v, year=range(2020, 2101, 10)).timeseries() / 1000
        _df = _df.groupby(['model', 'scenario']).sum()
        _df = pd.DataFrame(_df.apply(pyam.cumulative, raw=False, axis=1, first_year=2020, last_year=last_year))
        _df = pyam.filter_by_meta(_df, df, category=cats, marker=None, join_meta=True)

        for j, c in enumerate(cats):
            __df = _df[_df.category == c]
            lst = __df[0][~np.isnan(__df[0])]
            pos = 0.5 / _cats * (j - _cats / 2) + i

            outliers = len(lst[lst > ymax])
            if outliers > 0:
                plt.text(pos - 0.01 * len(cats), ymax * 1.01, outliers)

            p = plt.boxplot(lst, positions=[pos], widths=(0.3 / _cats),
                            whis='range',
                            patch_artist=True)
            plt.setp(p['boxes'], color=rc['color']['category'][c])
            plt.setp(p['medians'], color='black')

            for m in marker:
                val = __df.loc[_df.marker == m, 0]
                if not val.empty:
                    plt.scatter(x=pos, y=val, **marker_args(m),
                                s=40, label=None)

        label_list.append(name)

    for m in marker:
        meta = df.filter(marker=m).timeseries()
        if not meta.empty:
            meta = meta.iloc[0].name[0:2]
            plt.scatter(x=[], y=[], **marker_args(m), s=60, label=m)

    for j, c in enumerate(cats):
        plt.plot([], c=rc['color']['category'][c], label='{}'.format(c))

    if legend:
        plt.legend()

    plt.grid(False)
    plt.xlim(-0.6, (i + 0.6))
    plt.xticks(range(0, i + 1), label_list)
    plt.vlines(x=[_i + 0.5 for _i in range(i)], ymin=0, ymax=ymax, colors='white')
    plt.ylim(0, ymax)
    plt.ylabel('Cumulative CO2 until {} (GtCO2)'.format(last_year))

    fig.savefig('output/fig2.9{}_cdr_{}.png'.format(panel_label, last_year))
In [36]:
boxplot_cumulative_ccs(340, 2050, 'a')
In [37]:
boxplot_cumulative_ccs(1250, 2100, 'b', legend=False)

Export timeseries data to xlsx

In [38]:
df.to_excel('output/fig2.9_data_table.xlsx')
In [ ]: