sdom 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ from pyomo.environ import *
2
+ import os
3
+ import logging
4
+
5
+ def safe_pyomo_value(var):
6
+ """Return the value of a variable or expression if it is initialized, else return None."""
7
+ try:
8
+ return value(var) if var is not None else None
9
+ except ValueError:
10
+ return None
11
+
12
+ def check_file_exists(filepath, name_file = ""):
13
+ """Check if the expected file exists. Raise FileNotFoundError if not."""
14
+ if not os.path.isfile(filepath):
15
+ logging.error(f"Expected {name_file} file not found: {filepath}")
16
+ raise FileNotFoundError(f"Expected {name_file} file not found: {filepath}")
17
+ return False
18
+ return True
sdom/config_sdom.py ADDED
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+ def configure_logging(level=logging.INFO, log_file=None):
4
+ """
5
+ Configures logging for the module.
6
+ Args:
7
+ level: Logging level (default: logging.INFO)
8
+ log_file: Optional file path to log to a file
9
+ """
10
+ handlers = [logging.StreamHandler()]
11
+ if log_file:
12
+ handlers.append(logging.FileHandler(log_file))
13
+ logging.basicConfig(
14
+ level=level,
15
+ format='%(asctime)s %(levelname)s %(message)s',
16
+ handlers=handlers
17
+ )
sdom/constants.py ADDED
@@ -0,0 +1,29 @@
1
+ # INCLUDE HERE ALL THE CONSTATS AND USE UPPER CASE NAMES
2
+
3
+ INPUT_CSV_NAMES = {
4
+ 'solar_plants': 'Set_k_SolarPV.csv', #TODO this set should be optional since are col names CFSolar_2050.csv
5
+ 'wind_plants': 'Set_w_Wind.csv', #TODO this set should be optional since are col names CFWind_2050.csv
6
+ 'load_data': 'Load_hourly_2050.csv',
7
+ 'nuclear_data': 'Nucl_hourly_2019.csv',
8
+ 'large_hydro_data': 'lahy_hourly_2019.csv',
9
+ 'other_renewables_data': 'otre_hourly_2019.csv',
10
+ 'cf_solar': 'CFSolar_2050.csv',
11
+ 'cf_wind': 'CFWind_2050.csv',
12
+ 'cap_solar': 'CapSolar_2050.csv',
13
+ 'cap_wind': 'CapWind_2050.csv',
14
+ 'storage_data': 'StorageData_2050.csv',
15
+ 'scalars': 'Scalars.csv',
16
+ }
17
+
18
+ VRE_PROPERTIES_NAMES = ['trans_cap_cost', 'CAPEX_M', 'FOM_M']
19
+ STORAGE_PROPERTIES_NAMES = ['P_Capex', 'E_Capex', 'Eff', 'Min_Duration',
20
+ 'Max_Duration', 'Max_P', 'FOM', 'VOM', 'Lifetime', 'CostRatio']
21
+
22
+ #TODO this set is the col names of the StorageData_2050.csv file
23
+ STORAGE_SET_J_TECHS = ['Li-Ion', 'CAES', 'PHS', 'H2']
24
+ STORAGE_SET_B_TECHS = ['Li-Ion', 'PHS'] #TODO INCLUDE DECOUPLED STORAGE FLAG IN THE CSV FILE
25
+
26
+ #RESILIENCY CONSTANTS HARD-CODED
27
+ # PCLS - Percentage of Critical Load Served - Constraint : Resilience
28
+ CRITICAL_LOAD_PERCENTAGE = 1 # 10% of the total load
29
+ PCLS_TARGET = 0.9 # 90% of the total load
@@ -0,0 +1,111 @@
1
+ import logging
2
+ from pyomo.environ import *
3
+ from .constants import STORAGE_PROPERTIES_NAMES, STORAGE_SET_J_TECHS, STORAGE_SET_B_TECHS
4
+ from .models.formulations_vre import add_vre_parameters
5
+ from .models.formulations_thermal import add_gascc_parameters
6
+ from .models.formulations_nuclear import add_nuclear_parameters
7
+ from .models.formulations_hydro import add_large_hydro_parameters
8
+ from .models.formulations_other_renewables import add_other_renewables_parameters
9
+ from .models.formulations_load import add_load_parameters
10
+ from .models.formulations_storage import add_storage_parameters
11
+ from .models.formulations_resiliency import add_resiliency_parameters
12
+ from .models.models_utils import crf_rule
13
+
14
+ def initialize_sets( model, data, n_hours = 8760 ):
15
+ """
16
+ Initialize model sets from the provided data dictionary.
17
+
18
+ Args:
19
+ model: The optimization model instance to initialize.
20
+ data: A dictionary containing model parameters and data.
21
+ """
22
+ # Solar plant ID alignment
23
+ solar_plants_cf = data['cf_solar'].columns[1:].astype(str).tolist()
24
+ solar_plants_cap = data['cap_solar']['sc_gid'].astype(str).tolist()
25
+ common_solar_plants = list(set(solar_plants_cf) & set(solar_plants_cap))
26
+
27
+ # Filter solar data and initialize model set
28
+ complete_solar_data = data["cap_solar"][data["cap_solar"]['sc_gid'].astype(str).isin(common_solar_plants)]
29
+ complete_solar_data = complete_solar_data.dropna(subset=['CAPEX_M', 'trans_cap_cost', 'FOM_M', 'capacity'])
30
+ common_solar_plants_filtered = complete_solar_data['sc_gid'].astype(str).tolist()
31
+ model.k = Set( initialize = common_solar_plants_filtered )
32
+
33
+ # Load the solar capacities
34
+ cap_solar_dict = complete_solar_data.set_index('sc_gid')['capacity'].to_dict()
35
+
36
+ # Filter the dictionary to ensure only valid keys are included
37
+ default_capacity_value = 0.0
38
+ filtered_cap_solar_dict = {k: cap_solar_dict.get(k, default_capacity_value) for k in model.k}
39
+
40
+ # Wind plant ID alignment
41
+ wind_plants_cf = data['cf_wind'].columns[1:].astype(str).tolist()
42
+ wind_plants_cap = data['cap_wind']['sc_gid'].astype(str).tolist()
43
+ common_wind_plants = list( set( wind_plants_cf ) & set( wind_plants_cap ) )
44
+
45
+ # Filter wind data and initialize model set
46
+ complete_wind_data = data["cap_wind"][data["cap_wind"]['sc_gid'].astype(str).isin(common_wind_plants)]
47
+ complete_wind_data = complete_wind_data.dropna(subset=['CAPEX_M', 'trans_cap_cost', 'FOM_M', 'capacity'])
48
+ common_wind_plants_filtered = complete_wind_data['sc_gid'].astype(str).tolist()
49
+ model.w = Set(initialize=common_wind_plants_filtered)
50
+
51
+ # Load the wind capacities
52
+ cap_wind_dict = complete_wind_data.set_index('sc_gid')['capacity'].to_dict()
53
+
54
+ # Filter the dictionary to ensure only valid keys are included
55
+ filtered_cap_wind_dict = {w: cap_wind_dict.get(w, default_capacity_value) for w in model.w}
56
+
57
+ #add to data dict new data pre-procesing dicts
58
+ data['filtered_cap_solar_dict'] = filtered_cap_solar_dict
59
+ data['filtered_cap_wind_dict'] = filtered_cap_wind_dict
60
+ data['complete_solar_data'] = complete_solar_data
61
+ data['complete_wind_data'] = complete_wind_data
62
+
63
+ # Define sets
64
+ model.h = RangeSet(1, n_hours)
65
+ model.j = Set( initialize = STORAGE_SET_J_TECHS )
66
+ model.b = Set( initialize = STORAGE_SET_B_TECHS )
67
+
68
+ # Initialize storage properties
69
+ model.sp = Set( initialize = STORAGE_PROPERTIES_NAMES )
70
+
71
+
72
+
73
+ def initialize_params(model, data):
74
+ """
75
+ Initialize model parameters from the provided data dictionary.
76
+
77
+ Args:
78
+ model: The optimization model instance to initialize.
79
+ data: A dictionary containing model parameters and data.
80
+ filtered_cap_solar_dict
81
+ """
82
+ model.r = Param( initialize = float(data["scalars"].loc["r"].Value) ) # Discount rate
83
+
84
+ logging.debug("--Initializing VRE parameters...")
85
+ add_vre_parameters(model, data)
86
+
87
+ logging.debug("--Initializing gas combined cycle parameters...")
88
+ add_gascc_parameters(model,data)
89
+
90
+ logging.debug("--Initializing load parameters...")
91
+ add_load_parameters(model, data)
92
+
93
+ logging.debug("--Initializing nuclear parameters...")
94
+ add_nuclear_parameters(model, data)
95
+
96
+ logging.debug("--Initializing large hydro parameters...")
97
+ add_large_hydro_parameters(model, data)
98
+
99
+ logging.debug("--Initializing other renewables parameters...")
100
+ add_other_renewables_parameters(model, data)
101
+
102
+ logging.debug("--Initializing storage parameters...")
103
+ add_storage_parameters(model, data)
104
+
105
+ # GenMix_Target, mutable to change across multiple runs
106
+ model.GenMix_Target = Param( initialize = float(data["scalars"].loc["GenMix_Target"].Value), mutable=True)
107
+ model.CRF = Param( model.j, initialize = crf_rule ) #Capital Recovery Factor
108
+
109
+ logging.debug("--Initializing resiliency parameters...")
110
+ add_resiliency_parameters(model, data)
111
+ #model.CRF.display()
sdom/io_manager.py ADDED
@@ -0,0 +1,468 @@
1
+ import logging
2
+ import pandas as pd
3
+ import os
4
+ import csv
5
+
6
+ from pyomo.environ import *
7
+
8
+ from .common.utilities import safe_pyomo_value, check_file_exists
9
+ from .constants import INPUT_CSV_NAMES
10
+
11
+
12
+ def load_data( input_data_dir = '.\\Data\\' ):
13
+ """
14
+ Loads the required SDOM datasets from CSV files located in the specified input directory.
15
+ Parameters:
16
+ input_data_dir (str): Path to the directory containing the input CSV files. Defaults to '.\\Data\\'.
17
+ Returns:
18
+ dict: A dictionary containing the following keys and their corresponding loaded data:
19
+ - "solar_plants" (list): List of solar plant identifiers.
20
+ - "wind_plants" (list): List of wind plant identifiers.
21
+ - "load_data" (pd.DataFrame): Hourly load data for the year 2050.
22
+ - "nuclear_data" (pd.DataFrame): Hourly nuclear generation data for 2019.
23
+ - "large_hydro_data" (pd.DataFrame): Hourly large hydro generation data for 2019.
24
+ - "other_renewables_data" (pd.DataFrame): Hourly other renewables generation data for 2019.
25
+ - "cf_solar" (pd.DataFrame): Solar capacity factors for 2050.
26
+ - "cf_wind" (pd.DataFrame): Wind capacity factors for 2050.
27
+ - "cap_solar" (pd.DataFrame): Solar plant capacities for 2050.
28
+ - "cap_wind" (pd.DataFrame): Wind plant capacities for 2050.
29
+ - "storage_data" (pd.DataFrame): Storage data for 2050, indexed by the first column.
30
+ - "scalars" (pd.DataFrame): Scalar parameters, indexed by the "Parameter" column.
31
+ Notes:
32
+ - All numeric data is rounded to 5 decimal places.
33
+ - Some columns are explicitly converted to string type for consistency.
34
+ """
35
+ logging.info("Loading SDOM input data...")
36
+ #os.chdir('./Data/.')
37
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["solar_plants"])
38
+ if check_file_exists(input_file_path, "solar plants ids"):
39
+ solar_plants = pd.read_csv( input_file_path, header=None )[0].tolist()
40
+
41
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["wind_plants"])
42
+ if check_file_exists(input_file_path, "wind plants ids"):
43
+ wind_plants = pd.read_csv( input_file_path, header=None )[0].tolist()
44
+
45
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["load_data"])
46
+ if check_file_exists(input_file_path, "load data"):
47
+ load_data = pd.read_csv( input_file_path ).round(5)
48
+
49
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["nuclear_data"])
50
+ if check_file_exists(input_file_path, "nuclear data"):
51
+ nuclear_data = pd.read_csv( input_file_path ).round(5)
52
+
53
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["large_hydro_data"])
54
+ if check_file_exists(input_file_path, "large hydro data"):
55
+ large_hydro_data = pd.read_csv( input_file_path ).round(5)
56
+
57
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["other_renewables_data"])
58
+ if check_file_exists(input_file_path, "other renewables data"):
59
+ other_renewables_data = pd.read_csv( input_file_path ).round(5)
60
+
61
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["cf_solar"])
62
+ if check_file_exists(input_file_path, "Capacity factors for pv solar"):
63
+ cf_solar = pd.read_csv( input_file_path ).round(5)
64
+ cf_solar.columns = cf_solar.columns.astype(str)
65
+
66
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["cf_wind"])
67
+ if check_file_exists(input_file_path, "Capacity factors for wind"):
68
+ cf_wind = pd.read_csv( input_file_path ).round(5)
69
+ cf_wind.columns = cf_wind.columns.astype(str)
70
+
71
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["cap_solar"])
72
+ if check_file_exists(input_file_path, "Capex information for solar"):
73
+ cap_solar = pd.read_csv( input_file_path ).round(5)
74
+ cap_solar['sc_gid'] = cap_solar['sc_gid'].astype(str)
75
+
76
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["cap_wind"])
77
+ if check_file_exists(input_file_path, "Capex information for wind"):
78
+ cap_wind = pd.read_csv( input_file_path ).round(5)
79
+ cap_wind['sc_gid'] = cap_wind['sc_gid'].astype(str)
80
+
81
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["storage_data"])
82
+ if check_file_exists(input_file_path, "Storage data"):
83
+ storage_data = pd.read_csv( input_file_path, index_col=0 ).round(5)
84
+
85
+ input_file_path = os.path.join(input_data_dir, INPUT_CSV_NAMES["scalars"])
86
+ if check_file_exists(input_file_path, "scalars"):
87
+ scalars = pd.read_csv( input_file_path, index_col="Parameter" )
88
+ #os.chdir('../')
89
+ return {
90
+ "solar_plants": solar_plants,
91
+ "wind_plants": wind_plants,
92
+ "load_data": load_data,
93
+ "nuclear_data": nuclear_data,
94
+ "large_hydro_data": large_hydro_data,
95
+ "other_renewables_data": other_renewables_data,
96
+ "cf_solar": cf_solar,
97
+ "cf_wind": cf_wind,
98
+ "cap_solar": cap_solar,
99
+ "cap_wind": cap_wind,
100
+ "storage_data": storage_data,
101
+ "scalars": scalars,
102
+ }
103
+
104
+
105
+
106
+
107
+ # ---------------------------------------------------------------------------------
108
+ # Export results to CSV files
109
+ # ---------------------------------------------------------------------------------
110
+
111
+ def export_results( model, case, output_dir = './results_pyomo/' ):
112
+ """
113
+ Exports optimization results from a Pyomo model to CSV files.
114
+ This function extracts generation, storage, and summary results from the provided Pyomo model instance,
115
+ organizes them into dictionaries and pandas DataFrames, and writes them to CSV files in the specified output directory.
116
+ Parameters
117
+ ----------
118
+ model : pyomo.environ.ConcreteModel
119
+ The Pyomo model instance containing the optimization results.
120
+ case : str or int
121
+ Identifier for the current simulation case or scenario. Used in output filenames.
122
+ output_dir : str, optional
123
+ Directory path where the output CSV files will be saved. Defaults to './results_pyomo/'.
124
+ Outputs
125
+ -------
126
+ OutputGeneration_{case}.csv : CSV file
127
+ Contains hourly generation and curtailment results for each technology and scenario.
128
+ OutputStorage_{case}.csv : CSV file
129
+ Contains hourly storage operation results (charging, discharging, state of charge) for each storage technology.
130
+ OutputSummary_{case}.csv : CSV file
131
+ Contains summary metrics including total cost, capacities, generation, demand, CAPEX, OPEX, and other key results.
132
+ Notes
133
+ -----
134
+ - The function assumes the model contains specific variables and sets (e.g., GenPV, CurtPV, GenWind, PC, PD, SOC, etc.).
135
+ - The function creates the output directory if it does not exist.
136
+ - Results are only written if relevant data is available (e.g., non-empty results).
137
+ """
138
+
139
+ logging.info("Exporting SDOM results...")
140
+ os.makedirs(output_dir, exist_ok=True)
141
+
142
+ # Initialize results dictionaries column: [values]
143
+ logging.debug("--Initializing results dictionaries...")
144
+ gen_results = {'Scenario':[],'Hour': [], 'Solar PV Generation (MW)': [], 'Solar PV Curtailment (MW)': [],
145
+ 'Wind Generation (MW)': [], 'Wind Curtailment (MW)': [],
146
+ 'Gas CC Generation (MW)': [], 'Storage Charge/Discharge (MW)': []}
147
+
148
+ storage_results = {'Hour': [], 'Technology': [], 'Charging power (MW)': [],
149
+ 'Discharging power (MW)': [], 'State of charge (MWh)': []}
150
+
151
+ summary_results_columns = ['Metric', 'Technology', 'Run', 'Optimal Value', 'Unit']
152
+ summary_results = pd.DataFrame(columns=summary_results_columns)
153
+
154
+ # Extract generation results
155
+ # for run in range(num_runs):
156
+ logging.debug("--Extracting generation results...")
157
+ for h in model.h:
158
+ solar_gen = safe_pyomo_value(model.GenPV[h])
159
+ solar_curt = safe_pyomo_value(model.CurtPV[h])
160
+ wind_gen = safe_pyomo_value(model.GenWind[h])
161
+ wind_curt = safe_pyomo_value(model.CurtWind[h])
162
+ gas_cc_gen = safe_pyomo_value(model.GenCC[h])
163
+
164
+ if None not in [solar_gen, solar_curt, wind_gen, wind_curt, gas_cc_gen]:
165
+ # gen_results['Scenario'].append(run)
166
+ gen_results['Hour'].append(h)
167
+ gen_results['Solar PV Generation (MW)'].append(solar_gen)
168
+ gen_results['Solar PV Curtailment (MW)'].append(solar_curt)
169
+ gen_results['Wind Generation (MW)'].append(wind_gen)
170
+ gen_results['Wind Curtailment (MW)'].append(wind_curt)
171
+ gen_results['Gas CC Generation (MW)'].append(gas_cc_gen)
172
+
173
+ power_to_storage = sum(safe_pyomo_value(model.PC[h, j]) or 0 for j in model.j) - sum(safe_pyomo_value(model.PD[h, j]) or 0 for j in model.j)
174
+ gen_results['Storage Charge/Discharge (MW)'].append(power_to_storage)
175
+ gen_results['Scenario'].append(case)
176
+
177
+ # Extract storage results
178
+ logging.debug("--Extracting storage results...")
179
+ for h in model.h:
180
+ for j in model.j:
181
+ charge_power = safe_pyomo_value(model.PC[h, j])
182
+ discharge_power = safe_pyomo_value(model.PD[h, j])
183
+ soc = safe_pyomo_value(model.SOC[h, j])
184
+ if None not in [charge_power, discharge_power, soc]:
185
+ storage_results['Hour'].append(h)
186
+ storage_results['Technology'].append(j)
187
+ storage_results['Charging power (MW)'].append(charge_power)
188
+ storage_results['Discharging power (MW)'].append(discharge_power)
189
+ storage_results['State of charge (MWh)'].append(soc)
190
+
191
+
192
+
193
+ # Summary results (total capacities and costs)
194
+ ## Total cost
195
+ logging.debug("--Extracting summary results...")
196
+ total_cost = pd.DataFrame.from_dict({'Total cost':[None, 1,safe_pyomo_value(model.Obj()), '$US']}, orient='index',
197
+ columns=['Technology','Run','Optimal Value', 'Unit'])
198
+ total_cost = total_cost.reset_index(names='Metric')
199
+ summary_results = pd.concat([summary_results, total_cost], ignore_index=True)
200
+ ## Total capacity
201
+ cap = {}
202
+ cap['GasCC'] = safe_pyomo_value(model.CapCC)
203
+ cap['Solar PV'] = sum(safe_pyomo_value(model.Ypv[k]) * model.CapSolar_CAPEX_M[k] for k in model.k)
204
+ cap['Wind'] = sum(safe_pyomo_value(model.Ywind[w]) * model.CapWind_CAPEX_M[w] for w in model.w)
205
+ cap['All'] = cap['GasCC'] + cap['Solar PV'] + cap['Wind']
206
+ capacities = pd.DataFrame.from_dict(cap, orient='index', columns=['Optimal Value'])
207
+ capacities = capacities.reset_index(names=['Technology'])
208
+ capacities['Run'] = 1
209
+ capacities['Unit'] = 'MW'
210
+ capacities['Metric'] = 'Capacity'
211
+ summary_results = pd.concat([summary_results, capacities], ignore_index=True)
212
+ ## Generation
213
+ gen = {}
214
+ gen['GasCC'] = safe_pyomo_value(sum(model.GenCC[h] for h in model.h))
215
+ gen['Solar PV'] = sum(safe_pyomo_value(model.GenPV[h]) for h in model.h)
216
+ gen['Wind'] = sum(safe_pyomo_value(model.GenWind[h]) for h in model.h)
217
+ gen['Other renewables'] = safe_pyomo_value(sum(model.OtherRenewables[h]for h in model.h))
218
+ gen['Hydro'] = safe_pyomo_value(sum(model.LargeHydro[h] for h in model.h))
219
+ gen['Nuclear'] = safe_pyomo_value(sum(model.Nuclear[h] for h in model.h))
220
+ gen['LiIon'] = safe_pyomo_value(sum(model.PD[h, 'Li-Ion'] for h in model.h))
221
+ gen['CAES'] = safe_pyomo_value(sum(model.PD[h, 'CAES'] for h in model.h) )
222
+ gen['PHS'] = safe_pyomo_value(sum(model.PD[h, 'PHS'] for h in model.h) )
223
+ gen['H2'] = safe_pyomo_value(sum(model.PD[h, 'H2'] for h in model.h) )
224
+ gen['All'] = gen['GasCC'] + gen['Solar PV'] + gen['Wind'] + gen['Other renewables'] + gen['Hydro'] + \
225
+ gen['Nuclear'] + gen['LiIon'] + gen['CAES'] + gen['PHS'] + gen['H2']
226
+ generation = pd.DataFrame.from_dict(gen, orient='index', columns=['Optimal Value'])
227
+ generation = generation.reset_index(names=['Technology'])
228
+ generation['Run'] = 1
229
+ generation['Unit'] = 'MWh'
230
+ generation['Metric'] = 'Total generation'
231
+ summary_results = pd.concat([summary_results, generation], ignore_index=True)
232
+ ## Storage energy charging
233
+ stoch = {}
234
+ stoch['LiIon'] = safe_pyomo_value(sum(model.PC[h, 'Li-Ion'] for h in model.h))
235
+ stoch['CAES'] = safe_pyomo_value(sum(model.PC[h, 'CAES'] for h in model.h) )
236
+ stoch['PHS'] = safe_pyomo_value(sum(model.PC[h, 'PHS'] for h in model.h) )
237
+ stoch['H2'] = safe_pyomo_value(sum(model.PC[h, 'H2'] for h in model.h) )
238
+ stoch['All'] = stoch['LiIon'] + stoch['CAES'] + stoch['PHS'] + stoch['H2']
239
+ storage_charging = pd.DataFrame.from_dict(stoch, orient='index', columns=['Optimal Value'])
240
+ storage_charging = storage_charging.reset_index(names=['Technology'])
241
+ storage_charging['Run'] = 1
242
+ storage_charging['Unit'] = 'MWh'
243
+ storage_charging['Metric'] = 'Storage energy charging'
244
+ summary_results = pd.concat([summary_results, storage_charging], ignore_index=True)
245
+ ## Demand
246
+ dem = {}
247
+ dem['demand'] = sum(model.Load[h] for h in model.h)
248
+ demand = pd.DataFrame.from_dict(dem, orient='index', columns=['Optimal Value'])
249
+ demand = demand.reset_index(names=['Technology'])
250
+ demand['Run'] = 1
251
+ demand['Unit'] = 'MWh'
252
+ demand['Metric'] = 'Total demand'
253
+ summary_results = pd.concat([summary_results, demand], ignore_index=True)
254
+ ## CAPEX
255
+ capex = {}
256
+ capex['Solar PV'] = sum(safe_pyomo_value((model.FCR_VRE * (1000 * model.CapSolar_CAPEX_M[k] + model.CapSolar_trans_cap_cost[k]))\
257
+ * model.CapSolar_capacity[k] * model.Ypv[k]) for k in model.k)
258
+ capex['Wind'] = sum(safe_pyomo_value((model.FCR_VRE * (1000 * model.CapWind_CAPEX_M[w] + model.CapWind_trans_cap_cost[w])) \
259
+ * model.CapWind_capacity[w] * model.Ywind[w]) for w in model.w)
260
+ capex['GasCC'] = safe_pyomo_value(model.FCR_GasCC*1000*model.CapexGasCC*model.CapCC)
261
+ capex['All'] = capex['Solar PV'] + capex['Wind'] + capex['GasCC']
262
+ capital_costs = pd.DataFrame.from_dict(capex, orient='index', columns=['Optimal Value'])
263
+ capital_costs = capital_costs.reset_index(names=['Technology'])
264
+ capital_costs['Run'] = 1
265
+ capital_costs['Unit'] = '$US'
266
+ capital_costs['Metric'] = 'CAPEX'
267
+ summary_results = pd.concat([summary_results, capital_costs], ignore_index=True)
268
+ ## Power CAPEX
269
+ pcapex = {}
270
+ pcapex['LiIon'] = safe_pyomo_value(model.CRF['Li-Ion']*(1000*model.StorageData['CostRatio', 'Li-Ion'] * \
271
+ model.StorageData['P_Capex', 'Li-Ion']*model.Pcha['Li-Ion']
272
+ + 1000*(1 - model.StorageData['CostRatio', 'Li-Ion']) * \
273
+ model.StorageData['P_Capex', 'Li-Ion']*model.Pdis['Li-Ion']))
274
+ pcapex['CAES'] = safe_pyomo_value(model.CRF['CAES']*(1000*model.StorageData['CostRatio', 'CAES'] * \
275
+ model.StorageData['P_Capex', 'CAES']*model.Pcha['CAES']
276
+ + 1000*(1 - model.StorageData['CostRatio', 'CAES']) * \
277
+ model.StorageData['P_Capex', 'CAES']*model.Pdis['CAES']))
278
+ pcapex['PHS'] = safe_pyomo_value(model.CRF['PHS']*(1000*model.StorageData['CostRatio', 'PHS'] * \
279
+ model.StorageData['P_Capex', 'PHS']*model.Pcha['PHS']
280
+ + 1000*(1 - model.StorageData['CostRatio', 'PHS']) * \
281
+ model.StorageData['P_Capex', 'PHS']*model.Pdis['PHS']))
282
+ pcapex['H2'] = safe_pyomo_value(model.CRF['H2']*(1000*model.StorageData['CostRatio', 'H2'] * \
283
+ model.StorageData['P_Capex', 'H2']*model.Pcha['H2']
284
+ + 1000*(1 - model.StorageData['CostRatio', 'H2']) * \
285
+ model.StorageData['P_Capex', 'H2']*model.Pdis['H2']))
286
+ pcapex['All'] = pcapex['LiIon'] + pcapex['CAES'] + pcapex['PHS'] + pcapex['H2']
287
+ power_capex = pd.DataFrame.from_dict(pcapex, orient='index',columns=['Optimal Value'])
288
+ power_capex = power_capex.reset_index(names=['Technology'])
289
+ power_capex['Run'] = 1
290
+ power_capex['Unit'] = '$US'
291
+ power_capex['Metric'] = 'Power-CAPEX'
292
+ summary_results = pd.concat([summary_results, power_capex], ignore_index=True)
293
+ ## Energy CAPEX
294
+ ecapex = {}
295
+ ecapex['LiIon'] = safe_pyomo_value(model.CRF['Li-Ion']*1000*model.StorageData['E_Capex', 'Li-Ion']*model.Ecap['Li-Ion'])
296
+ ecapex['CAES'] = safe_pyomo_value(model.CRF['CAES']*1000*model.StorageData['E_Capex', 'CAES']*model.Ecap['CAES'])
297
+ ecapex['PHS'] = safe_pyomo_value(model.CRF['PHS']*1000*model.StorageData['E_Capex', 'PHS']*model.Ecap['PHS'])
298
+ ecapex['H2'] = safe_pyomo_value(model.CRF['H2']*1000*model.StorageData['E_Capex', 'H2']*model.Ecap['H2'])
299
+ ecapex['All'] = ecapex['LiIon'] + ecapex['CAES'] + ecapex['PHS'] + ecapex['H2']
300
+ energy_capex = pd.DataFrame.from_dict(ecapex, orient='index',columns=['Optimal Value'])
301
+ energy_capex = energy_capex.reset_index(names=['Technology'])
302
+ energy_capex['Run'] = 1
303
+ energy_capex['Unit'] = '$US'
304
+ energy_capex['Metric'] = 'Energy-CAPEX'
305
+ summary_results = pd.concat([summary_results, energy_capex], ignore_index=True)
306
+ ## Total CAPEX
307
+ tcapex = {}
308
+ tcapex['LiIon'] = pcapex['LiIon'] + ecapex['LiIon']
309
+ tcapex['CAES'] = pcapex['CAES'] + ecapex['CAES']
310
+ tcapex['PHS'] = pcapex['PHS'] + ecapex['PHS']
311
+ tcapex['H2'] = pcapex['H2'] + ecapex['H2']
312
+ tcapex['All'] = tcapex['LiIon'] + tcapex['CAES'] + tcapex['PHS'] + tcapex['H2']
313
+ total_capex = pd.DataFrame.from_dict(tcapex, orient='index',columns=['Optimal Value'])
314
+ total_capex = total_capex.reset_index(names=['Technology'])
315
+ total_capex['Run'] = 1
316
+ total_capex['Unit'] = '$US'
317
+ total_capex['Metric'] = 'Total-CAPEX'
318
+ summary_results = pd.concat([summary_results, total_capex], ignore_index=True)
319
+ ## FOM
320
+ fom = {}
321
+ fom['GasCC'] = safe_pyomo_value(1000*model.FOM_GasCC*model.CapCC)
322
+ fom['Solar PV'] = sum(safe_pyomo_value((model.FCR_VRE * 1000*model.CapSolar_FOM_M[k]) * model.CapSolar_capacity[k] * model.Ypv[k]) for k in model.k)
323
+ fom['Wind'] = sum(safe_pyomo_value((model.FCR_VRE * 1000*model.CapWind_FOM_M[w]) * model.CapWind_capacity[w] * model.Ywind[w]) for w in model.w)
324
+ fom['LiIon'] = safe_pyomo_value(1000*model.StorageData['CostRatio', 'Li-Ion'] * model.StorageData['FOM', 'Li-Ion']*model.Pcha['Li-Ion']
325
+ + 1000*(1 - model.StorageData['CostRatio', 'Li-Ion']) * model.StorageData['FOM', 'Li-Ion']*model.Pdis['Li-Ion'])
326
+ fom['CAES'] = safe_pyomo_value(1000*model.StorageData['CostRatio', 'CAES'] * model.StorageData['FOM', 'CAES']*model.Pcha['CAES']
327
+ + 1000*(1 - model.StorageData['CostRatio', 'CAES']) * model.StorageData['FOM', 'CAES']*model.Pdis['CAES'])
328
+ fom['PHS'] = safe_pyomo_value(1000*model.StorageData['CostRatio', 'PHS'] * model.StorageData['FOM', 'PHS']*model.Pcha['PHS']
329
+ + 1000*(1 - model.StorageData['CostRatio', 'PHS']) * model.StorageData['FOM', 'PHS']*model.Pdis['PHS'])
330
+ fom['H2'] = safe_pyomo_value(1000*model.StorageData['CostRatio', 'H2'] * model.StorageData['FOM', 'H2']*model.Pcha['H2']
331
+ + 1000*(1 - model.StorageData['CostRatio', 'H2']) * model.StorageData['FOM', 'H2']*model.Pdis['H2'])
332
+ fom['All'] = fom['GasCC'] + fom['Solar PV'] + fom['Wind'] + fom['LiIon'] + fom['CAES'] + fom['PHS'] + fom['H2']
333
+ fixedom = pd.DataFrame.from_dict(fom, orient='index', columns=['Optimal Value'])
334
+ fixedom = fixedom.reset_index(names=['Technology'])
335
+ fixedom['Run'] = 1
336
+ fixedom['Unit'] = '$US'
337
+ fixedom['Metric'] = 'FOM'
338
+ summary_results = pd.concat([summary_results, fixedom], ignore_index=True)
339
+ ## VOM
340
+ vom = {}
341
+ vom['GasCC'] = safe_pyomo_value((model.GasPrice * model.HR + model.VOM_GasCC) *sum(model.GenCC[h] for h in model.h))
342
+ vom['LiIon'] = safe_pyomo_value(model.StorageData['VOM', 'Li-Ion'] * sum(model.PD[h, 'Li-Ion'] for h in model.h))
343
+ vom['CAES'] = safe_pyomo_value(model.StorageData['VOM', 'CAES'] * sum(model.PD[h, 'CAES'] for h in model.h) )
344
+ vom['PHS'] = safe_pyomo_value(model.StorageData['VOM', 'PHS'] * sum(model.PD[h, 'PHS'] for h in model.h) )
345
+ vom['H2'] = safe_pyomo_value(model.StorageData['VOM', 'H2'] * sum(model.PD[h, 'H2'] for h in model.h) )
346
+ vom['All'] = vom['GasCC'] + vom['LiIon'] + vom['CAES'] + vom['PHS'] + vom['H2']
347
+ variableom = pd.DataFrame.from_dict(vom, orient='index', columns=['Optimal Value'])
348
+ variableom = variableom.reset_index(names=['Technology'])
349
+ variableom['Run'] = 1
350
+ variableom['Unit'] = '$US'
351
+ variableom['Metric'] = 'VOM'
352
+ summary_results = pd.concat([summary_results, variableom], ignore_index=True)
353
+ ## OPEX
354
+ opex = {}
355
+ opex['GasCC'] = fom['GasCC'] + vom['GasCC']
356
+ opex['Solar PV'] = fom['Solar PV']
357
+ opex['Wind'] = fom['Wind']
358
+ opex['LiIon'] = fom['LiIon'] + vom['LiIon']
359
+ opex['CAES'] = fom['CAES'] + vom['CAES']
360
+ opex['PHS'] = fom['PHS'] + vom['PHS']
361
+ opex['H2'] = fom['H2'] + vom['H2']
362
+ opex['All'] = opex['GasCC'] + opex['Solar PV'] + opex['Wind'] + opex['LiIon'] + opex['CAES'] + opex['PHS'] + opex['H2']
363
+ operating_cost = pd.DataFrame.from_dict(opex, orient='index', columns=['Optimal Value'])
364
+ operating_cost = operating_cost.reset_index(names=['Technology'])
365
+ operating_cost['Run'] = 1
366
+ operating_cost['Unit'] = '$US'
367
+ operating_cost['Metric'] = 'OPEX'
368
+ summary_results = pd.concat([summary_results, operating_cost], ignore_index=True)
369
+ ## Charge power capacity
370
+ charge = {}
371
+ charge['LiIon'] = safe_pyomo_value(model.Pcha['Li-Ion'])
372
+ charge['CAES'] = safe_pyomo_value(model.Pcha['CAES'])
373
+ charge['PHS'] = safe_pyomo_value(model.Pcha['PHS'])
374
+ charge['H2'] = safe_pyomo_value(model.Pcha['H2'])
375
+ charge['All'] = charge['LiIon'] + charge['CAES'] + charge['PHS'] + charge['H2']
376
+ charge_power = pd.DataFrame.from_dict(charge, orient='index', columns=['Optimal Value'])
377
+ charge_power = charge_power.reset_index(names=['Technology'])
378
+ charge_power['Run'] = 1
379
+ charge_power['Unit'] = 'MW'
380
+ charge_power['Metric'] = 'Charge power capacity'
381
+ summary_results = pd.concat([summary_results, charge_power], ignore_index=True)
382
+ ## Discharge power capacity
383
+ dcharge = {}
384
+ dcharge['LiIon'] = safe_pyomo_value(model.Pdis['Li-Ion'] )
385
+ dcharge['CAES'] = safe_pyomo_value(model.Pdis['CAES'])
386
+ dcharge['PHS'] = safe_pyomo_value(model.Pdis['PHS'])
387
+ dcharge['H2'] = safe_pyomo_value(model.Pdis['H2'])
388
+ dcharge['All'] = dcharge['LiIon'] + dcharge['CAES'] + dcharge['PHS'] + dcharge['H2']
389
+ dcharge_power = pd.DataFrame.from_dict(dcharge, orient='index', columns=['Optimal Value'])
390
+ dcharge_power = dcharge_power.reset_index(names=['Technology'])
391
+ dcharge_power['Run'] = 1
392
+ dcharge_power['Unit'] = 'MW'
393
+ dcharge_power['Metric'] = 'Discharge power capacity'
394
+ summary_results = pd.concat([summary_results, dcharge_power], ignore_index=True)
395
+ ## Average power capacity
396
+ avgpocap = {}
397
+ avgpocap['LiIon'] = (charge['LiIon'] + dcharge['LiIon'])/2
398
+ avgpocap['CAES'] = (charge['CAES'] + dcharge['CAES'])/2
399
+ avgpocap['PHS'] = (charge['PHS'] + dcharge['PHS'])/2
400
+ avgpocap['H2'] = (charge['H2'] + dcharge['H2'])/2
401
+ avgpocap['All'] = avgpocap['LiIon'] + avgpocap['CAES'] + avgpocap['PHS'] + avgpocap['H2']
402
+ average_power = pd.DataFrame.from_dict(avgpocap, orient='index', columns=['Optimal Value'])
403
+ average_power = average_power.reset_index(names=['Technology'])
404
+ average_power['Run'] = 1
405
+ average_power['Unit'] = 'MW'
406
+ average_power['Metric'] = 'Average power capacity'
407
+ summary_results = pd.concat([summary_results, average_power], ignore_index=True)
408
+ ## Energy capacity
409
+ encap = {}
410
+ encap['LiIon'] = safe_pyomo_value(model.Ecap['Li-Ion'] )
411
+ encap['CAES'] = safe_pyomo_value(model.Ecap['CAES'])
412
+ encap['PHS'] = safe_pyomo_value(model.Ecap['PHS'])
413
+ encap['H2'] = safe_pyomo_value(model.Ecap['H2'])
414
+ encap['All'] = encap['LiIon'] + encap['CAES'] + encap['PHS'] + encap['H2']
415
+ energy_cap = pd.DataFrame.from_dict(encap, orient='index', columns=['Optimal Value'])
416
+ energy_cap = energy_cap.reset_index(names=['Technology'])
417
+ energy_cap['Run'] = 1
418
+ energy_cap['Unit'] = 'MWh'
419
+ energy_cap['Metric'] = 'Energy capacity'
420
+ summary_results = pd.concat([summary_results, energy_cap], ignore_index=True)
421
+ ## Discharge duration
422
+ dur = {}
423
+ dur['LiIon'] = safe_pyomo_value(sqrt(model.StorageData['Eff','Li-Ion']*model.Ecap['Li-Ion']/(model.Pdis['Li-Ion'] + 1e-15)))
424
+ dur['CAES'] = safe_pyomo_value(sqrt(model.StorageData['Eff','CAES']*model.Ecap['CAES']/(model.Pdis['CAES'] + 1e-15)))
425
+ dur['PHS'] = safe_pyomo_value(sqrt(model.StorageData['Eff','PHS']*model.Ecap['PHS']/(model.Pdis['PHS'] + 1e-15)))
426
+ dur['H2'] = safe_pyomo_value(sqrt(model.StorageData['Eff','H2']*model.Ecap['H2']/(model.Pdis['H2'] + 1e-15)))
427
+ duration = pd.DataFrame.from_dict(dur, orient='index', columns=['Optimal Value'])
428
+ duration = duration.reset_index(names=['Technology'])
429
+ duration['Run'] = 1
430
+ duration['Unit'] = 'h'
431
+ duration['Metric'] = 'Duration'
432
+ summary_results = pd.concat([summary_results, duration], ignore_index=True)
433
+ ## Equivalent number of cycles
434
+ cyc = {}
435
+ cyc['LiIon'] = safe_pyomo_value(gen['LiIon']/(model.Ecap['Li-Ion'] + 1e-15))
436
+ cyc['CAES'] = safe_pyomo_value(gen['CAES']/(model.Ecap['CAES'] + 1e-15))
437
+ cyc['PHS'] = safe_pyomo_value(gen['PHS']/(model.Ecap['PHS'] + 1e-15))
438
+ cyc['H2'] = safe_pyomo_value(gen['H2']/(model.Ecap['H2']+ 1e-15))
439
+ cycles = pd.DataFrame.from_dict(cyc, orient='index', columns=['Optimal Value'])
440
+ cycles = cycles.reset_index(names=['Technology'])
441
+ cycles['Run'] = 1
442
+ cycles['Unit'] = '-'
443
+ cycles['Metric'] = 'Equivalent number of cycles'
444
+ summary_results = pd.concat([summary_results, cycles], ignore_index=True)
445
+
446
+ logging.info("Exporting csv files containing SDOM results...")
447
+ # Save generation results to CSV
448
+ logging.debug("--Saving generation results to CSV...")
449
+ if gen_results['Hour']:
450
+ with open(output_dir + f'OutputGeneration_{case}.csv', mode='w', newline='') as file:
451
+ writer = csv.DictWriter(file, fieldnames=gen_results.keys())
452
+ writer.writeheader()
453
+ writer.writerows([dict(zip(gen_results, t))
454
+ for t in zip(*gen_results.values())])
455
+
456
+ # Save storage results to CSV
457
+ logging.debug("--Saving storage results to CSV...")
458
+ if storage_results['Hour']:
459
+ with open(output_dir + f'OutputStorage_{case}.csv', mode='w', newline='') as file:
460
+ writer = csv.DictWriter(file, fieldnames=storage_results.keys())
461
+ writer.writeheader()
462
+ writer.writerows([dict(zip(storage_results, t))
463
+ for t in zip(*storage_results.values())])
464
+
465
+ # Save summary results to CSV
466
+ logging.debug("--Saving summary results to CSV...")
467
+ if len(summary_results)>0:
468
+ summary_results.to_csv(output_dir + f'OutputSummary_{case}.csv', index=False)