@@ -20,18 +20,25 @@ def __init__(
2020 is stored in the model_library/model_name folder. The required files are:
2121 1. transmission.csv: A file that contains the transmission data.
2222 2. thermal_unit.csv: A file that contains the thermal unit data.
23- 3. unit_marginal_cost .csv: A file that contains the marginal cost of non-thermal units .
24- 4. (Optional) solar .csv, wind.csv, hydropower.csv, import.csv: Files that contain the renewable unit data.
23+ 3. solar .csv, wind.csv, hydropower.csv, import.csv: Files that contain the renewable unit data .
24+ 4. energy_storage .csv: A file that contains the energy storage system data.
2525 """
2626 self .input_folder = input_folder
2727 self .model_name = model_name
2828 self .year = year
2929 self .frequency = frequency
3030
3131 # Values that will be calculated
32- self .cycle_map : dict = {}
33- self .thermal_derate_factors : pd .DataFrame = get_dates (year )
34- self .marginal_costs : pd .DataFrame = get_dates (year )
32+ self .cycle_map : dict = json .loads ("{}" )
33+ self .thermal_derate_factors : pd .DataFrame = pd .DataFrame ()
34+ self .thermal_derated_capacity : pd .DataFrame = pd .DataFrame ()
35+
36+ self .transmission_data : pd .DataFrame = pd .DataFrame ()
37+ self .transmission_params : dict = {} # Default PowNet parameters
38+ self .user_transmission : pd .DataFrame = pd .DataFrame ()
39+
40+ self .ess_derate_factors : pd .DataFrame = pd .DataFrame ()
41+ self .ess_derated_capacity : pd .DataFrame = pd .DataFrame ()
3542
3643 # Maps frequency to wavelength
3744 wavelengths = {50 : 6000 , 60 : 5000 }
@@ -40,7 +47,7 @@ def __init__(
4047 # Note that we will modify the original file
4148 self .model_folder = os .path .join (self .input_folder , model_name )
4249
43- def load_data (self ) -> None :
50+ def load_transmission_data (self ) -> None :
4451 # User inputs of transmission data
4552 self .user_transmission = pd .read_csv (
4653 os .path .join (self .model_folder , "transmission.csv" ),
@@ -141,6 +148,9 @@ def calc_line_capacity(self) -> None:
141148 """Calculate the capacity of line segments. The unit is in MW.
142149 Line capacity is the minimum of the thermal limit and the steady-state
143150 stability limit (a function of distance).
151+
152+ Note the calculated values are overwritten by user provided values
153+ in the transmission.csv file.
144154 """
145155 self .transmission_data ["stability_limit" ] = self .user_transmission .apply (
146156 lambda x : self .calc_stability_limit (
@@ -166,6 +176,19 @@ def calc_line_capacity(self) -> None:
166176 ["thermal_limit" , "stability_limit" ]
167177 ].min (axis = 1 )
168178
179+ # Overwrite calculated values with user provided values
180+ excluded_list = [- 1 , None ]
181+ user_specified_capacity = self .user_transmission .loc [
182+ ~ self .user_transmission ["user_line_cap" ].isin (excluded_list )
183+ ]
184+ user_specified_capacity = user_specified_capacity .set_index (
185+ ["source" , "sink" ]
186+ ).rename (columns = {"user_line_cap" : "line_capacity" })
187+
188+ self .transmission_data = self .transmission_data .set_index (["source" , "sink" ])
189+ self .transmission_data .update (user_specified_capacity )
190+ self .transmission_data = self .transmission_data .reset_index ()
191+
169192 def calc_line_susceptance (self ) -> None :
170193 """Calculate the susceptance of line segments. The unit is in Siemens (S)."""
171194 # Assume reactance based on the maximum voltage level of the two buses
@@ -178,16 +201,34 @@ def calc_line_susceptance(self) -> None:
178201 axis = 1 ,
179202 )
180203
181- self .transmission_data ["reactance_pu " ] = (
204+ self .transmission_data ["reactance " ] = (
182205 self .transmission_data ["reactance_per_km" ]
183206 * self .user_transmission ["distance" ]
184207 )
185208
186209 self .transmission_data ["susceptance" ] = self .transmission_data .apply (
187- lambda x : int (x ["source_kv" ] * x ["sink_kv" ] / x ["reactance_pu " ]),
210+ lambda x : int (x ["source_kv" ] * x ["sink_kv" ] / x ["reactance " ]),
188211 axis = 1 ,
189212 )
190213
214+ # Replace with user-specified values
215+ excluded_values = [- 1 , None ]
216+ user_specified_susceptance = self .user_transmission .loc [
217+ ~ self .user_transmission ["susceptance" ].isin (excluded_values ),
218+ ["source" , "sink" , "susceptance" ],
219+ ]
220+ user_specified_susceptance = user_specified_susceptance .set_index (
221+ ["source" , "sink" ]
222+ )
223+ # Change from float to int
224+ user_specified_susceptance = user_specified_susceptance .astype (
225+ {"susceptance" : int }
226+ )
227+
228+ self .transmission_data = self .transmission_data .set_index (["source" , "sink" ])
229+ self .transmission_data .update (user_specified_susceptance )
230+ self .transmission_data = self .transmission_data .reset_index ()
231+
191232 def write_transmission_data (self ) -> None :
192233 self .transmission_data .to_csv (
193234 os .path .join (self .model_folder , "pownet_transmission.csv" ), index = False
@@ -205,7 +246,7 @@ def create_cycle_map(self) -> None:
205246 target = "sink" ,
206247 )
207248 cycles = nx .cycle_basis (graph )
208- # We save this map to use by the ModelBuilder
249+ # Save this map to be uses by ModelBuilder
209250 self .cycle_map = {f"cycle_{ idx + 1 } " : cycle for idx , cycle in enumerate (cycles )}
210251
211252 def write_cycle_map (self ) -> None :
@@ -216,99 +257,128 @@ def write_cycle_map(self) -> None:
216257 with open (os .path .join (self .model_folder , "pownet_cycle_map.json" ), "w" ) as f :
217258 json .dump (self .cycle_map , f )
218259
219- def create_thermal_derate_factors (self , derate_factor : float = 1.00 ) -> None :
220- """Assumes a constant derate factor for all thermal units. The derate factor is applied
221- to the nameplate capacity of thermal units.
260+ def _create_derate_factors (
261+ self , unit_type : str , derate_factor : float = 1.00
262+ ) -> None :
263+ """Creates derate factors for a given unit type (thermal or ess).
264+
265+ Args:
266+ unit_type (str): The type of unit ('thermal' or 'ess').
267+ derate_factor (float): The derate factor to apply. Defaults to 1.00.
222268 """
223- # Get the thermal units
269+
224270 model_dir = os .path .join (self .input_folder , self .model_name )
225- thermal_units = pd .read_csv (os .path .join (model_dir , "thermal_unit.csv" ))[
226- "name"
227- ].values
271+
272+ if unit_type == "thermal" :
273+ filename = "thermal_unit.csv"
274+ attribute_name = "thermal_derate_factors"
275+ elif unit_type == "ess" :
276+ filename = "energy_storage.csv"
277+ attribute_name = "ess_derate_factors"
278+ else :
279+ raise ValueError (
280+ f"Invalid unit type: { unit_type } . Must be 'thermal' or 'ess'."
281+ )
282+
283+ if os .path .exists (os .path .join (model_dir , filename )):
284+ units = pd .read_csv (os .path .join (model_dir , filename ))["name" ].values
285+ else :
286+ return
287+
228288 temp_df = pd .DataFrame (
229289 derate_factor ,
230- index = self . thermal_derate_factors . index ,
231- columns = thermal_units ,
290+ index = range ( 0 , 8760 ), # Consider making 8760 a constant or parameter
291+ columns = units ,
232292 )
233- self .thermal_derate_factors = pd .concat (
234- [get_dates (year = self .year ), temp_df ], axis = 1
293+ setattr (
294+ self ,
295+ attribute_name ,
296+ pd .concat ([get_dates (year = self .year ), temp_df ], axis = 1 ),
235297 )
236298
299+ def create_thermal_derate_factors (self , derate_factor : float = 1.00 ) -> None :
300+ """Creates derate factors for thermal units."""
301+ self ._create_derate_factors ("thermal" , derate_factor )
302+
303+ def create_ess_derate_factors (self , derate_factor : float = 1.00 ) -> None :
304+ """Creates derate factors for ESS units."""
305+ self ._create_derate_factors ("ess" , derate_factor )
306+
237307 def write_thermal_derate_factors (self ) -> None :
238308 self .thermal_derate_factors .to_csv (
239309 os .path .join (self .model_folder , "pownet_derate_factor.csv" ), index = False
240310 )
241311
242- def create_derated_capacity (self ) -> None :
243- """Create a dataframe of hourly derated capacity of thermal units. The columns are names of thermal units."""
244- # Get the nameplate capacity of each thermal unit
245- max_cap = pd . read_csv (
246- os . path . join ( self . model_folder , "thermal_unit.csv" ),
247- header = 0 ,
248- index_col = "name" ,
249- usecols = [ "name" , "max_capacity" ],
250- ). to_dict ()[ "max_capacity" ]
251-
252- self . derated_max_cap = pd . DataFrame (
253- 0 ,
254- columns = max_cap . keys (),
255- index = range ( 0 , 8760 ), # match index with get_dates
256- )
257- for thermal_unit in max_cap . keys () :
258- self . derated_max_cap [ thermal_unit ] = (
259- self . thermal_derate_factors [ thermal_unit ] * max_cap [ thermal_unit ]
312+ def _create_derated_capacity (self , unit_type : str ) -> None :
313+ """Creates a dataframe of hourly derated capacity for a given unit type.
314+
315+ Args:
316+ unit_type (str): The type of unit ('thermal' or 'ess').
317+ """
318+
319+ if unit_type == "thermal" :
320+ filename = "thermal_unit.csv"
321+ derate_factor_attr = "thermal_derate_factors"
322+ derated_capacity_attr = "thermal_derated_capacity"
323+ elif unit_type == "ess" :
324+ filename = "energy_storage.csv"
325+ derate_factor_attr = "ess_derate_factors"
326+ derated_capacity_attr = "ess_derated_capacity"
327+ else :
328+ raise ValueError (
329+ f"Invalid unit type: { unit_type } . Must be 'thermal' or 'ess'."
260330 )
261331
262- self .derated_max_cap = pd .concat (
263- [get_dates (year = self .year ), self .derated_max_cap ], axis = 1
332+ # Get the nameplate capacity of each unit
333+ filepath = os .path .join (self .model_folder , filename )
334+ if os .path .exists (filepath ):
335+ max_cap = pd .read_csv (
336+ filepath ,
337+ index_col = "name" ,
338+ usecols = ["name" , "max_capacity" ],
339+ )[
340+ "max_capacity"
341+ ] # Directly get the Series
342+ else :
343+ return
344+
345+ # Get the derate factors for the units
346+ derate_factors = getattr (self , derate_factor_attr )
347+
348+ # Efficiently calculate derated capacity using vectorized operations
349+ derated_capacity = derate_factors .drop (columns = ["date" , "hour" ]).mul (
350+ max_cap , axis = 1
264351 )
265- self .derated_max_cap .index += 1
266352
267- def write_derated_capacity ( self ) -> None :
268- self . derated_max_cap . to_csv (
269- os . path . join ( self .model_folder , "pownet_derated_capacity.csv" ), index = False
353+ # Concatenate with dates and set the index
354+ derated_capacity = pd . concat (
355+ [ get_dates ( year = self .year ), derated_capacity ], axis = 1
270356 )
357+ derated_capacity .index += 1
271358
272- def create_marginal_costs (self ) -> None :
273- """Create a dataframe of hourly fuel prices (or marginal costs) of non-thermal units.
274- The columns are names of renewable and import units. The marginal cost is in $/MWh.
275- """
276- # unit_marginal_cost.csv has three columns: name, fuel_type, and marginal_cost.
277- # Now, we create a fuel_type to marginal_cost mapping
278- constant_prices = pd .read_csv (
279- os .path .join (self .model_folder , "unit_marginal_cost.csv" ),
280- header = 0 ,
281- index_col = "fuel_type" ,
282- ).to_dict ()["marginal_cost" ]
283-
284- # If there are solar.csv, hydropower.csv, and wind.csv files, then we need to
285- # include them in the fuel price file.
286- hours_in_year = range (8760 )
287- unit_types = ["solar" , "wind" , "hydropower" , "import" ]
288- for unit_type in unit_types :
289- filename = os .path .join (self .model_folder , f"{ unit_type } .csv" )
290- if os .path .exists (filename ):
291- units = pd .read_csv (filename , header = 0 ).columns
292- units = units .drop (
293- ["year" , "month" , "day" , "hour" ], errors = "ignore"
294- ).to_list ()
295- temp_df = pd .DataFrame (
296- constant_prices [unit_type ],
297- index = hours_in_year ,
298- columns = units ,
299- )
300- self .marginal_costs = pd .concat (
301- [
302- self .marginal_costs ,
303- temp_df ,
304- ],
305- axis = 1 ,
306- )
307-
308- def write_marginal_costs (self ) -> None :
309- self .marginal_costs .to_csv (
310- os .path .join (self .model_folder , "pownet_marginal_cost.csv" ), index = False
311- )
359+ setattr (self , derated_capacity_attr , derated_capacity )
360+
361+ def create_thermal_derated_capacity (self ) -> None :
362+ """Creates a dataframe of hourly derated capacity of thermal units."""
363+ self ._create_derated_capacity ("thermal" )
364+
365+ def create_ess_derated_capacity (self ) -> None :
366+ """Creates a dataframe of hourly derated capacity of ess units."""
367+ self ._create_derated_capacity ("ess" )
368+
369+ def write_thermal_derated_capacity (self ) -> None :
370+ if not self .thermal_derate_factors .empty :
371+ self .thermal_derated_capacity .to_csv (
372+ os .path .join (self .model_folder , "pownet_thermal_derated_capacity.csv" ),
373+ index = False ,
374+ )
375+
376+ def write_ess_derated_capacity (self ) -> None :
377+ if not self .ess_derated_capacity .empty :
378+ self .ess_derated_capacity .to_csv (
379+ os .path .join (self .model_folder , "pownet_ess_derated_capacity.csv" ),
380+ index = False ,
381+ )
312382
313383 def check_user_line_capacities (self ) -> None :
314384 """The user can provide their own line capacities under user_line_cap column
@@ -319,22 +389,30 @@ def check_user_line_capacities(self) -> None:
319389
320390 def run_all_processing_steps (self ) -> None :
321391 """Run all the data processing steps"""
322- self .calc_line_capacity ()
323- self .calc_line_susceptance ()
324- self .create_cycle_map ()
392+ if not self .user_transmission .empty :
393+ self .calc_line_capacity ()
394+ self .calc_line_susceptance ()
395+ self .create_cycle_map ()
396+
325397 self .create_thermal_derate_factors ()
326- self .create_derated_capacity ()
327- self .create_marginal_costs ()
398+ self .create_thermal_derated_capacity ()
399+
400+ self .create_ess_derate_factors ()
401+ self .create_ess_derated_capacity ()
328402
329403 def write_data (self ) -> None :
330404 """Write the processed data as csv files sharing a prefix "pownet_" to the model folder"""
331- self .write_transmission_data ()
332- self .write_cycle_map ()
333- self .write_thermal_derate_factors ()
334- self .write_derated_capacity ()
335- self .write_marginal_costs ()
405+
406+ if not self .transmission_data .empty :
407+ self .write_transmission_data ()
408+ self .write_cycle_map ()
409+
410+ self .write_thermal_derated_capacity ()
411+ self .write_ess_derated_capacity ()
336412
337413 def execute_data_pipeline (self ) -> None :
338- self .load_data ()
414+ if os .path .exists (os .path .join (self .model_folder , "transmission.csv" )):
415+ self .load_transmission_data ()
416+
339417 self .run_all_processing_steps ()
340418 self .write_data ()
0 commit comments