Harmonization with Carbon Budget Conservation¶
We seek an emissions trajectory consistent with a provided historical emissions timeseries that closely matches a modeled result and maintains the overall carbon budget consistent with that model result.
First, model and history data are read in. The model is then harmonized. Finally, output is analyzed.
[1]:
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import pyomo.environ as pyomo
import aneris
from aneris.tutorial import load_data
%matplotlib inline
The driver is used to execute the harmonization. It will handle the data formatting needed to execute the harmonizaiton operation and stores the harmonized results until they are needed.
Some logging output is provided. It can be suppressed with
aneris.logger().setLevel('WARN')
[2]:
model, hist, driver = load_data()
Since the default function which chooses, which method to use does not apply the budget method, we specify overrides to use budget for all the variables in the model data.
[3]:
driver.overrides = model[["Model", "Scenario", "Region", "Variable", "Unit"]].assign(Method="budget")
driver.overrides.head()
[3]:
| Model | Scenario | Region | Variable | Unit | Method | |
|---|---|---|---|---|---|---|
| 0 | model | sspn | regionc | prefix|Emissions|BC|sector1|suffix | Mt BC/yr | budget |
| 1 | model | sspn | regionc | prefix|Emissions|BC|sector2|suffix | Mt BC/yr | budget |
| 2 | model | sspn | regionc | prefix|Emissions|BC|suffix | Mt BC/yr | budget |
| 3 | model | sspn | World | prefix|Emissions|BC|sector1|suffix | Mt BC/yr | budget |
| 4 | model | sspn | World | prefix|Emissions|BC|sector2|suffix | Mt BC/yr | budget |
[4]:
for scenario in driver.scenarios():
driver.harmonize(scenario)
harmonized, metadata, diagnostics = driver.harmonized_results()
INFO:root:Downselecting prefix|suffix variables
INFO:root:Translating to standard format
INFO:root:Aggregating historical values to native regions
INFO:root:Harmonizing (with example methods):
WARNING:root:Removing override methods not in processed model output:
region gas sector units
World BC prefix|sector1|suffix kt budget
prefix|sector2|suffix kt budget
prefix|suffix kt budget
Name: method, dtype: object
INFO:root: method default override
region gas sector units
regionc BC prefix|sector1|suffix kt budget reduce_ratio_2050 budget
prefix|sector2|suffix kt budget reduce_ratio_2050 budget
prefix|suffix kt budget reduce_ratio_2050 budget
INFO:root:and override methods:
INFO:root:region gas sector units
World BC prefix|sector1|suffix kt budget
prefix|sector2|suffix kt budget
prefix|suffix kt budget
regionc BC prefix|sector1|suffix kt budget
prefix|sector2|suffix kt budget
Name: method, dtype: object
WARNING:root:Removing override methods not in processed model output:
region gas sector units
World BC prefix|sector1|suffix kt budget
prefix|sector2|suffix kt budget
prefix|suffix kt budget
Name: method, dtype: object
INFO:root:Harmonizing with budget
WARNING:root:Removing sector aggregates. Recalculating with harmonized totals.
INFO:root:Translating to IAMC template
All data of interest is combined in order to easily view it. We will specifically investigate output for the World in this example. A few operations are performed in order to get the data into a plotting-friendly format.
[5]:
data = pd.concat([hist, model, harmonized], sort=True)
df = data[data.Region == 'World']
[6]:
df = pd.melt(df, id_vars=aneris.iamc_idx, value_vars=aneris.numcols(df),
var_name='Year', value_name='Emissions')
df['Label'] = df['Model'] + ' ' + df['Variable']
[7]:
df.head()
[7]:
| Model | Scenario | Region | Variable | Year | Emissions | Label | |
|---|---|---|---|---|---|---|---|
| 0 | History | scen | World | prefix|Emissions|BC|sector1|suffix | 2000 | 4.0 | History prefix|Emissions|BC|sector1|suffix |
| 1 | History | scen | World | prefix|Emissions|BC|sector2|suffix | 2000 | 6.0 | History prefix|Emissions|BC|sector2|suffix |
| 2 | History | scen | World | prefix|Emissions|BC|suffix | 2000 | 10.0 | History prefix|Emissions|BC|suffix |
| 3 | model | sspn | World | prefix|Emissions|BC|sector1|suffix | 2000 | NaN | model prefix|Emissions|BC|sector1|suffix |
| 4 | model | sspn | World | prefix|Emissions|BC|sector2|suffix | 2000 | NaN | model prefix|Emissions|BC|sector2|suffix |
[8]:
sns.lineplot(x='Year', y='Emissions', hue='Label', data=df.assign(Year=df.Year.astype(int)))
plt.legend(bbox_to_anchor=(1.05, 1))
[8]:
<matplotlib.legend.Legend at 0x7fb304eefc10>
Calculation details¶
In the following we explain the method in more detail by harmonizing manually only the total prefix|Emissions|BC|suffix in the World region
[9]:
ms = df.loc[(df.Variable == 'prefix|Emissions|BC|suffix') & (df.Model == 'model')].set_index('Year')['Emissions'].dropna()
hs = df.loc[(df.Variable == 'prefix|Emissions|BC|suffix') & (df.Model == 'History')].set_index('Year')['Emissions'].dropna()
We calculate the carbon budget from the model and historical data by estimating the integral between discrete data points using the Riemann trapezoidal sum.
[10]:
def calc_budget(data):
# trapezoid rule reimann sum
dx = data.index.to_series().astype(int).diff()
y1 = data
dy = data.diff()
budget = (dx * (y1 - .5 * dy)).iloc[1:].sum()
return budget
calc_budget(ms) / 1e3 # in Gt BC
[10]:
2.99
Harmonization via Optimization¶
We create a model that matches as close as possible the dynamic rates of change of the modeled emission trajectory but adhering to the original carbon budget of the model.
This takes in as data:
the year values of the harmonized data
the original model values (unharmonized)
the historical value to be harmonized to (assumed to occur in the first year)
the total carbon budget to match
[11]:
years = ms.index[ms.index.astype(int) >= int(hs.index[-1])]
model_vals = ms.loc[years]
hist_val = hs.iloc[-1]
budget = calc_budget(model_vals)
The model itself minimizes the \(L_2\) norm of the rates of change:
Given model results, \(m_i\), years, \(y_i\), and harmonized results, \(x_i\).
[12]:
model = pyomo.ConcreteModel()
model.x = pyomo.Var(list(years), initialize=0, domain=pyomo.Reals)
x = pd.Series([model.x[y] for y in years], years)
delta_years = years.to_series().astype(int).diff()
delta_x = x.diff()
delta_m = model_vals.diff()
def l2_norm():
return pyomo.quicksum(((delta_m / delta_years - delta_x / delta_years) ** 2).dropna())
model.obj = pyomo.Objective(expr=l2_norm(), sense=pyomo.minimize)
The historical value must match
[13]:
model.hist_val = pyomo.Constraint(expr=model.x[years[0]] == hist_val)
And the carbon budget must be maintained, using a trapezoidal rule Reimann sum,
[14]:
model.budget = pyomo.Constraint(expr=calc_budget(x) == budget)
The model is solved with IPOPT and compared with the original trajectory.
[15]:
solver = pyomo.SolverFactory('ipopt')
solver.solve(model)
[15]:
{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 2, 'Number of variables': 11, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0256350040435791}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}
[16]:
data = pd.concat(
dict(
model=ms,
history=hs,
model_harmonized=pd.Series([pyomo.value(model.x[y]) for y in years], years)
),
axis=1,
sort=True
)
data.index = data.index.astype(int)
data.plot.line()
[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fb3050a57c0>
[ ]: