Uncertainty Analysis¶
In this tutorial, we will perform an uncertainty analysis using the EnergyScope model. We will explore how varying certain parameters affects the energy system configuration and results. This is achieved by generating a sequence of parameter values using Sobol sequences, running multiple optimizations, and analyzing the outcomes.
Import Necessary Libraries¶
First, we import all the required libraries and modules:
import pandas as pd
import numpy as np
import seaborn as sns
from energyscope.energyscope import Energyscope
from energyscope.models import infrastructure_ch_2050
from energyscope.result import postprocessing
from energyscope.datasets import gen_sobol_sequence
pandas
: For data manipulation and analysis.numpy
: For numerical operations.seaborn
: For statistical data visualization.Energyscope
: Main class for initializing and running the EnergyScope model.infrastructure_ch_2050
: Predefined model for different configurations.postprocessing
: Functions to process results after optimizations.gen_sobol_sequence
: Function to generate Sobol sequences for parameter sampling.
Manual Parameter Definition¶
Define Parameters for Uncertainty Analysis¶
We manually define a list of parameters whose uncertainty we want to analyze. Each parameter includes a name, a lower bound, and an upper bound:
varying_parameter = 'c_inv'
parameters = [
{'name': 'PV_LV', 'lower_bound': 0, 'upper_bound': 50},
{'name': 'WIND', 'lower_bound': 0, 'upper_bound': 20},
{'name': 'CCGT', 'lower_bound': 0, 'upper_bound': 10}
]
PV_LV
: Photovoltaic at Low Voltage level.WIND
: Wind energy capacity.CCGT
: Combined Cycle Gas Turbine capacity.- The bounds represent the range within which the parameter values will vary during the analysis.
Generate Sobol Sequence for Parameter Sampling¶
We use the gen_sobol_sequence
function to generate a Sobol sequence, which provides quasi-random samples of parameter values within the specified bounds:
seq, prob = gen_sobol_sequence(parameters=parameters, trajectories=4)
trajectories=4
: Specifies the number of trajectories or samples to generate.seq
: The generated sequence of parameter values.prob
: Metadata about the parameters and their sampling.
Display Number of Generated Samples¶
We can display the number of generated samples to verify:
display(len(seq))
20
- This should output the total number of parameter sets generated.
Prepare Parameter DataFrame¶
We prepare a DataFrame to hold the generated parameter sequences in a format suitable for the EnergyScope model:
df = pd.DataFrame(seq, columns=prob['names']).T
df.columns = ['value' + str(x) for x in list(df.columns) if not str(x) == "nan"]
df = df.reset_index(names=['index0'])
df['param'] = varying_parameter
df['index1'] = np.nan
df['index2'] = np.nan
df['index3'] = np.nan
df
index0 | value0 | value1 | value2 | value3 | value4 | value5 | value6 | value7 | value8 | ... | value14 | value15 | value16 | value17 | value18 | value19 | param | index1 | index2 | index3 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | PV_LV | 4.6875 | 32.8125 | 4.6875 | 4.6875 | 32.8125 | 29.6875 | 7.8125 | 29.6875 | 29.6875 | ... | 45.3125 | 17.1875 | 20.3125 | 17.1875 | 17.1875 | 20.3125 | c_inv | NaN | NaN | NaN |
1 | WIND | 9.3750 | 9.3750 | 5.6250 | 9.3750 | 5.6250 | 19.3750 | 19.3750 | 15.6250 | 19.3750 | ... | 10.6250 | 14.3750 | 14.3750 | 0.6250 | 14.3750 | 0.6250 | c_inv | NaN | NaN | NaN |
2 | CCGT | 4.6875 | 4.6875 | 4.6875 | 9.6875 | 9.6875 | 9.6875 | 9.6875 | 9.6875 | 4.6875 | ... | 2.1875 | 7.1875 | 7.1875 | 7.1875 | 7.1875 | 7.1875 | c_inv | NaN | NaN | NaN |
3 rows × 25 columns
- Transpose the DataFrame: To match the expected format.
- Rename Columns: Prefix columns with
'value'
. - Reset Index: To ensure the parameters are properly indexed.
- Add Parameter Type: We specify that these values correspond to
'c_inv'
(investment costs). - Add Empty Indices: Additional indices are added for compatibility with the model's expectations.
Define Solver Options¶
We specify solver options to control the optimization process:
solver_options = {
'solver': 'gurobi',
'presolve_eps': 5e-9,
'presolve_fixeps': 7e-10,
'mipgap': 1e-10,
'display_eps': 1e-10,
'omit_zero_rows': 1,
'omit_zero_cols': 1,
'show_stats': 0,
'solver_msg': 0,
'cplex_options': 'integrality=5e-7',
}
'solver': 'gurobi'
: Specifies that the Gurobi solver should be used.- Other options fine-tune the solver's precision and output behavior.
Initialize and Run the Model with Parameter Variations¶
Initialize the EnergyScope Model¶
We create an instance of the EnergyScope model using the infrastructure_ch_2050 dataset and the specified solver options:
es_infra_ch = Energyscope(model=infrastructure_ch_2050, solver_options=solver_options)
Run Multiple Optimizations with Varying Parameters¶
We use the calc_sequence
method to run the model multiple times, each time with a different set of parameters from the Sobol sequence:
results_ch_n = es_infra_ch.calc_sequence(df)
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) Cell In[8], line 1 ----> 1 results_ch_n = es_infra_ch.calc_sequence(df) File /builds/energyscope/energyscope/src/energyscope/energyscope.py:262, in Energyscope.calc_sequence(self, data, parser, ds) 258 raise ValueError(f"Column '{col}' contains non-numeric values that could not be converted.") 261 # If AMPL has not been initialized, do so now --> 262 if self.es_model.getSets().__len__() == 0: 263 self._initial_run(ds=ds) 265 # Map each unique 'param' to the AMPL parameter object File /builds/energyscope/energyscope/src/energyscope/energyscope.py:30, in Energyscope.es_model(self) 28 else: 29 try: ---> 30 self.__es_model = AMPL(Environment(os.environ["AMPL_PATH"])) 31 except SystemError: 32 # Try to create the object a second time to prevent errors when starting `ampl_lic` 33 self.__es_model = AMPL() File /usr/local/lib/python3.10/os.py:680, in _Environ.__getitem__(self, key) 677 value = self._data[self.encodekey(key)] 678 except KeyError: 679 # raise KeyError with the original key value --> 680 raise KeyError(key) from None 681 return self.decodevalue(value) KeyError: 'AMPL_PATH'
# Postcompute KPIs
results_ch_n = postprocessing(results_ch_n, df_monthly=True)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[9], line 2 1 # Postcompute KPIs ----> 2 results_ch_n = postprocessing(results_ch_n, df_monthly=True) NameError: name 'results_ch_n' is not defined
df_monthly=True
: Indicates that monthly data should be included in the post-processing.
Extract Annual Results¶
We extract the annual results DataFrame for further analysis:
df_annual = results_ch_n.postprocessing['df_annual']
df_annual = df_annual.loc[:, ['F_Mult']]
df_annual = df_annual.reset_index()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[10], line 1 ----> 1 df_annual = results_ch_n.postprocessing['df_annual'] 2 df_annual = df_annual.loc[:, ['F_Mult']] 3 df_annual = df_annual.reset_index() NameError: name 'results_ch_n' is not defined
F_Mult
: Represents the flow multipliers or capacities of technologies.
Pivot the Results DataFrame¶
We pivot the DataFrame to have technologies as columns and runs as rows:
df_annual = pd.pivot_table(df_annual, values='F_Mult', index=['Run'], columns='level_0')
df_annual.sample(5)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[11], line 1 ----> 1 df_annual = pd.pivot_table(df_annual, values='F_Mult', index=['Run'], columns='level_0') 2 df_annual.sample(5) NameError: name 'df_annual' is not defined
cols = ['CCGT_CC', 'DEC_COGEN_GAS', 'DEC_HP_ELEC', 'GASIFICATION_H2', 'IND_BOILER_WASTE', 'IND_COGEN_WASTE', 'PV_LV', 'WIND']
display(sns.pairplot(df_annual[cols].fillna(0), diag_kind="kde"))
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[12], line 2 1 cols = ['CCGT_CC', 'DEC_COGEN_GAS', 'DEC_HP_ELEC', 'GASIFICATION_H2', 'IND_BOILER_WASTE', 'IND_COGEN_WASTE', 'PV_LV', 'WIND'] ----> 2 display(sns.pairplot(df_annual[cols].fillna(0), diag_kind="kde")) NameError: name 'df_annual' is not defined
cols
: List of technologies to include in the plot.fillna(0)
: Replaces NaN values with zero for plotting purposes.diag_kind="kde"
: Uses Kernel Density Estimation for the diagonal plots.
Semi-Automated Sequence Creation¶
In this section, we automate the generation of parameter sequences based on existing model parameters.
Initialize the Model¶
We initialize the EnergyScope model again:
es_infra_ch = Energyscope(model=infrastructure_ch_2050)
Run the Base Model¶
We perform an initial calculation to obtain baseline parameter values:
# Solve the model
results_ch = es_infra_ch.calc()
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) Cell In[14], line 2 1 # Solve the model ----> 2 results_ch = es_infra_ch.calc() File /builds/energyscope/energyscope/src/energyscope/energyscope.py:72, in Energyscope.calc(self, ds, parser) 68 def calc(self, ds: Dataset = None, parser: Callable[[AMPL], Result] = parse_result) -> Result: 69 """ 70 Calls AMPL with `df` as .dat and returns the parsed result. 71 """ ---> 72 if self.es_model.getSets().__len__() == 0: # Check if AMPL instance is empty 73 self._initial_run(ds=ds) 75 # Solve the model File /builds/energyscope/energyscope/src/energyscope/energyscope.py:30, in Energyscope.es_model(self) 28 else: 29 try: ---> 30 self.__es_model = AMPL(Environment(os.environ["AMPL_PATH"])) 31 except SystemError: 32 # Try to create the object a second time to prevent errors when starting `ampl_lic` 33 self.__es_model = AMPL() File /usr/local/lib/python3.10/os.py:680, in _Environ.__getitem__(self, key) 677 value = self._data[self.encodekey(key)] 678 except KeyError: 679 # raise KeyError with the original key value --> 680 raise KeyError(key) from None 681 return self.decodevalue(value) KeyError: 'AMPL_PATH'
Extract and Adjust Investment Costs¶
We extract the investment costs (c_inv
) from the model parameters and define new bounds for uncertainty analysis:
c_inv = results_ch.parameters['c_inv'].drop('Run', axis=1)
c_inv['lower_bound'] = 0.5 * c_inv['c_inv']
c_inv['upper_bound'] = 1.5 * c_inv['c_inv']
c_inv = c_inv.reset_index(names=['name']).drop('c_inv', axis=1)
c_inv = c_inv.loc[c_inv['lower_bound'] != 0, :]
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[15], line 1 ----> 1 c_inv = results_ch.parameters['c_inv'].drop('Run', axis=1) 2 c_inv['lower_bound'] = 0.5 * c_inv['c_inv'] 3 c_inv['upper_bound'] = 1.5 * c_inv['c_inv'] NameError: name 'results_ch' is not defined
- Adjust Bounds: We set the lower and upper bounds to ±50% of the original investment costs.
- Filter Out Zero Costs: Remove parameters where the lower bound is zero.
Select Key Technologies¶
Due to computational limitations, we select a subset of technologies to include in the uncertainty analysis:
selected_technologies = ['PV_LV', 'PV_MV', 'PV_HV', 'PV_EHV', 'DEC_HP_ELEC', 'WIND', 'GASIFICATION_H2', 'GASIFICATION_SNG']
c_inv = c_inv.loc[c_inv['name'].isin(selected_technologies), :]
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[16], line 2 1 selected_technologies = ['PV_LV', 'PV_MV', 'PV_HV', 'PV_EHV', 'DEC_HP_ELEC', 'WIND', 'GASIFICATION_H2', 'GASIFICATION_SNG'] ----> 2 c_inv = c_inv.loc[c_inv['name'].isin(selected_technologies), :] NameError: name 'c_inv' is not defined
selected_technologies
: List of technologies chosen for analysis.
Convert Parameters to Dictionary Format¶
We convert the DataFrame of parameters to a list of dictionaries required by the gen_sobol_sequence
function:
c_inv = c_inv.to_dict(orient='records')
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[17], line 1 ----> 1 c_inv = c_inv.to_dict(orient='records') NameError: name 'c_inv' is not defined
Generate Sobol Sequence for Selected Parameters¶
We generate a Sobol sequence for the selected parameters:
seq, prob = gen_sobol_sequence(parameters=c_inv, trajectories=8)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[18], line 1 ----> 1 seq, prob = gen_sobol_sequence(parameters=c_inv, trajectories=8) NameError: name 'c_inv' is not defined
trajectories=8
: Generates more samples for a more detailed analysis.
Display Number of Generated Samples¶
display(len(seq))
20
Prepare Parameter DataFrame¶
We prepare the DataFrame for the new sequence:
df = pd.DataFrame(seq, columns=prob['names']).T
df.columns = ['value' + str(x) for x in list(df.columns) if not str(x) == "nan"]
df = df.reset_index(names=['index0'])
df['param'] = 'c_inv'
df['index1'] = np.nan
df['index2'] = np.nan
df['index3'] = np.nan
df
index0 | value0 | value1 | value2 | value3 | value4 | value5 | value6 | value7 | value8 | ... | value14 | value15 | value16 | value17 | value18 | value19 | param | index1 | index2 | index3 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | PV_LV | 4.6875 | 32.8125 | 4.6875 | 4.6875 | 32.8125 | 29.6875 | 7.8125 | 29.6875 | 29.6875 | ... | 45.3125 | 17.1875 | 20.3125 | 17.1875 | 17.1875 | 20.3125 | c_inv | NaN | NaN | NaN |
1 | WIND | 9.3750 | 9.3750 | 5.6250 | 9.3750 | 5.6250 | 19.3750 | 19.3750 | 15.6250 | 19.3750 | ... | 10.6250 | 14.3750 | 14.3750 | 0.6250 | 14.3750 | 0.6250 | c_inv | NaN | NaN | NaN |
2 | CCGT | 4.6875 | 4.6875 | 4.6875 | 9.6875 | 9.6875 | 9.6875 | 9.6875 | 9.6875 | 4.6875 | ... | 2.1875 | 7.1875 | 7.1875 | 7.1875 | 7.1875 | 7.1875 | c_inv | NaN | NaN | NaN |
3 rows × 25 columns
solver_options = {
'solver': 'gurobi',
'presolve_eps': 5e-9,
'presolve_fixeps': 7e-10,
'mipgap': 1e-10,
'display_eps': 1e-10,
'omit_zero_rows': 1,
'omit_zero_cols': 1,
'show_stats': 0,
'solver_msg': 0,
'cplex_options': 'integrality=5e-7',
}
Initialize the Model with Solver Options¶
es_infra_ch = Energyscope(model=infrastructure_ch_2050, solver_options=solver_options)
Run Multiple Optimizations¶
We run the model multiple times with the new parameter sets:
results_ch_n = es_infra_ch.calc_sequence(df)
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) Cell In[23], line 1 ----> 1 results_ch_n = es_infra_ch.calc_sequence(df) File /builds/energyscope/energyscope/src/energyscope/energyscope.py:262, in Energyscope.calc_sequence(self, data, parser, ds) 258 raise ValueError(f"Column '{col}' contains non-numeric values that could not be converted.") 261 # If AMPL has not been initialized, do so now --> 262 if self.es_model.getSets().__len__() == 0: 263 self._initial_run(ds=ds) 265 # Map each unique 'param' to the AMPL parameter object File /builds/energyscope/energyscope/src/energyscope/energyscope.py:30, in Energyscope.es_model(self) 28 else: 29 try: ---> 30 self.__es_model = AMPL(Environment(os.environ["AMPL_PATH"])) 31 except SystemError: 32 # Try to create the object a second time to prevent errors when starting `ampl_lic` 33 self.__es_model = AMPL() File /usr/local/lib/python3.10/os.py:680, in _Environ.__getitem__(self, key) 677 value = self._data[self.encodekey(key)] 678 except KeyError: 679 # raise KeyError with the original key value --> 680 raise KeyError(key) from None 681 return self.decodevalue(value) KeyError: 'AMPL_PATH'
# Postcompute KPIs
results_ch_n = postprocessing(results_ch_n, df_monthly=False)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[24], line 2 1 # Postcompute KPIs ----> 2 results_ch_n = postprocessing(results_ch_n, df_monthly=False) NameError: name 'results_ch_n' is not defined
df_monthly=False
: We are focusing on annual data in this case.
Extract and Prepare Annual Results¶
df_annual = results_ch_n.postprocessing['df_annual']
df_annual = df_annual.loc[:, ['F_Mult']]
df_annual = df_annual.reset_index()
df_annual = pd.pivot_table(df_annual, values='F_Mult', index=['Run'], columns='level_0')
df_annual.sample(5)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[25], line 1 ----> 1 df_annual = results_ch_n.postprocessing['df_annual'] 2 df_annual = df_annual.loc[:, ['F_Mult']] 3 df_annual = df_annual.reset_index() NameError: name 'results_ch_n' is not defined
Visualize Technology Distribution¶
cols = ['CCGT_CC', 'DEC_COGEN_GAS', 'DEC_HP_ELEC', 'GASIFICATION_H2', 'IND_BOILER_WASTE', 'IND_COGEN_WASTE', 'PV_LV', 'WIND']
fig = sns.pairplot(df_annual[cols].fillna(0), diag_kind="kde")
fig.show(renderer="notebook")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[26], line 2 1 cols = ['CCGT_CC', 'DEC_COGEN_GAS', 'DEC_HP_ELEC', 'GASIFICATION_H2', 'IND_BOILER_WASTE', 'IND_COGEN_WASTE', 'PV_LV', 'WIND'] ----> 2 fig = sns.pairplot(df_annual[cols].fillna(0), diag_kind="kde") 3 fig.show(renderer="notebook") NameError: name 'df_annual' is not defined
- We plot the distributions to understand how the uncertainty in investment costs affects the capacities of different technologies.
By following these steps, we perform an uncertainty analysis on the EnergyScope model by varying the investment costs of selected technologies. This approach allows us to understand the sensitivity of the energy system configuration to changes in key parameters.
Note: Ensure that all the necessary data files and dependencies are correctly set up in your environment. The computational time may vary depending on the number of optimizations and the complexity of the model.