2323 - HydroCHIPPs (DoE-sponsored HydroWIRES project)
2424"""
2525import os
26+ import pandas as pd
2627import numpy as np
2728from datetime import datetime
2829from pownet .core import (
@@ -114,9 +115,10 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
114115 weekly_prices = np .round (np .random .uniform (5 , 240 , 168 ), 1 )
115116 weekly_contract_costs .append (weekly_prices )
116117
117- #%%########################################
118- # Input Processing and Simulation Loop
119- ###########################################
118+ ####################################
119+ # Input Processing and Simulation Loop
120+ #####################################
121+
120122# Optional: re-process input data from scratch
121123if to_process_inputs :
122124 data_processor = DataProcessor (
@@ -140,6 +142,7 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
140142 line_capacity_factor = 1 ,
141143 line_loss_factor = 0 ,
142144)
145+
143146inputs .load_and_check_data ()
144147
145148# Dictionaries to store results and models
@@ -154,27 +157,31 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
154157 update_weekly_hydro_capacity (inputs .weekly_hydro_capacity , start_day , first_values )
155158
156159 # === Apply updated weekly prices for this day ===
157- update_contract_costs (inputs , 'supplier' , weekly_contract_costs [start_day - 1 ], start_day )
158-
159- # Optional: push updates into the model (uncomment if needed)
160- # inputs.update_capacity()
160+ update_contract_costs (inputs , 'supplier' , weekly_contract_costs [start_day - 1 ], start_day )
161161
162162 # Initialize model and record-keeping objects
163163 model_builder = ModelBuilder (inputs )
164164 record = SystemRecord (inputs )
165+
165166 build_times = []
166167 opt_times = []
167168 objvals = []
168169
169- # Get initial condition state (e.g., generator status)
170- init_conditions = create_init_condition (inputs .thermal_units )
170+ # ========================================================================
171+ # CRITICAL FIX: Initialize with proper hydro initial conditions
172+ # ========================================================================
173+ init_conditions = create_init_condition (
174+ thermal_units = inputs .thermal_units ,
175+ storage_units = inputs .storage_units ,
176+ ess_max_capacity = inputs .ess_max_capacity ,
177+ hydro_units = inputs .hydro_units ,
178+ )
171179
172180 # === Inner loop: run multiple steps from this starting day ===
173181 if steps_to_run is None :
174- steps_to_run = 10 # 365 - (sim_horizon // 24 - 1)
182+ steps_to_run = 10
175183
176- #todo:select the start date
177- for step_k in range (start_day , start_day + steps_to_run - 1 ):
184+ for step_k in range (start_day , start_day + steps_to_run - 1 ):
178185 start_time = datetime .now ()
179186
180187 # Build or update the model for the current step
@@ -188,6 +195,7 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
188195 step_k = step_k ,
189196 init_conds = init_conditions ,
190197 )
198+
191199 build_times .append ((datetime .now () - start_time ).total_seconds ())
192200
193201 # Solve the model using HiGHS solver
@@ -206,9 +214,54 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
206214 runtime = power_system_model [start_day ].get_runtime (),
207215 objval = power_system_model [start_day ].get_objval (),
208216 solution = power_system_model [start_day ].get_solution (),
209- # lmp=power_system_model[start_day].solve_for_lmp(),
210217 step_k = step_k ,
211218 )
219+
220+ # ========================================================================
221+ # CRITICAL FIX: Extract hydro dispatch at t=24 for next iteration
222+ # ========================================================================
223+ if step_k == start_day :
224+ # Get the solution DataFrame
225+ first_solution = power_system_model [start_day ].get_solution ()
226+
227+ # Initialize dictionary to store hydro dispatch at t=24
228+ initial_phydro = {}
229+
230+ # Iterate through all variables to find phydro at t=24
231+ for idx , row in first_solution .iterrows ():
232+ varname = row ['varname' ]
233+
234+ # Check if this is a phydro variable
235+ if varname .startswith ('phydro[' ):
236+ # Extract unit and timestep using string operations
237+ # Format: phydro[unit, timestep]
238+ try :
239+ # Remove 'phydro[' prefix and ']' suffix
240+ content = varname [7 :- 1 ] # Skip 'phydro[' and ']'
241+
242+ # Split by comma
243+ parts = content .split (', ' )
244+
245+ if len (parts ) == 2 :
246+ unit = parts [0 ].strip ()
247+ timestep = int (parts [1 ].strip ())
248+
249+ # Only keep values at t=24
250+ if timestep == 24 :
251+ initial_phydro [unit ] = row ['value' ]
252+ except (ValueError , IndexError ) as e :
253+ print (f"⚠️ Warning: Could not parse varname '{ varname } ': { e } " )
254+ continue
255+
256+ # Update init_conditions if we found any hydro dispatch values
257+ if initial_phydro :
258+ init_conditions ['initial_phydro' ] = initial_phydro
259+ print (f"✓ Initialized hydro ramping for { len (initial_phydro )} units at step { step_k } " )
260+ print (f" Sample values: { dict (list (initial_phydro .items ())[:3 ])} " )
261+ else :
262+ print (f"⚠️ Warning: No hydro dispatch found at t=24 for step { step_k } " )
263+
264+ # Update initial conditions from record for subsequent iterations
212265 init_conditions = record .get_init_conds ()
213266
214267 #########################################
@@ -232,5 +285,128 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
232285 os .makedirs (output_folder , exist_ok = True )
233286 export_results [start_day ].to_csv (output_folder + f'results_day_{ str (start_day )} .csv' , index = False )
234287
288+ ####################################
289+ ##### Quick hydro ramping check ####
290+ ####################################
291+
292+ import matplotlib .pyplot as plt
293+
294+ # Extract hydro dispatch from last solved day
295+ final_day_results = export_results [start_day + steps_to_run - 2 ]
296+ hydro_data = final_day_results [final_day_results ['vartype' ] == 'phydro' ]
297+
298+ if not hydro_data .empty :
299+ # ========================================================================
300+ # CRITICAL FIX: Convert 'hour' to numeric and sort before pivoting
301+ # ========================================================================
302+ hydro_data = hydro_data .copy ()
303+ hydro_data ['hour' ] = pd .to_numeric (hydro_data ['hour' ], errors = 'coerce' )
304+
305+ # Drop any rows where hour couldn't be converted
306+ hydro_data = hydro_data .dropna (subset = ['hour' ])
307+
308+ # Convert to integer
309+ hydro_data ['hour' ] = hydro_data ['hour' ].astype (int )
310+
311+ # Sort by hour before pivoting
312+ hydro_data = hydro_data .sort_values ('hour' )
313+
314+ # Now pivot
315+ hydro_pivot = hydro_data .pivot (columns = 'unit' , index = 'hour' , values = 'value' )
316+
317+ # ========================================================================
318+ # VERIFY: Check if index is properly sorted
319+ # ========================================================================
320+ print (f"\n 📊 Hydro dispatch index check:" )
321+ print (f" First 5 hours: { hydro_pivot .index [:5 ].tolist ()} " )
322+ print (f" Last 5 hours: { hydro_pivot .index [- 5 :].tolist ()} " )
323+ print (f" Is sorted: { hydro_pivot .index .is_monotonic_increasing } " )
324+
325+ for unit in hydro_pivot .columns :
326+ dispatch = hydro_pivot [unit ]
327+
328+ # ========================================================================
329+ # CRITICAL: Ensure dispatch is sorted by index before diff()
330+ # ========================================================================
331+ dispatch = dispatch .sort_index ()
332+ ramp_changes = dispatch .diff ()
333+
334+ # ========================================================================
335+ # DIAGNOSTIC: Check for ramping violations
336+ # ========================================================================
337+ ramp_up_limit = inputs .hydro_RU .get (unit , 1e6 )
338+ ramp_down_limit = inputs .hydro_RD .get (unit , 1e6 )
339+
340+ violations_up = (ramp_changes > ramp_up_limit ).sum ()
341+ violations_down = (ramp_changes < - ramp_down_limit ).sum ()
342+
343+ if violations_up > 0 or violations_down > 0 :
344+ print (f"\n ⚠️ RAMPING VIOLATIONS DETECTED for { unit } :" )
345+ print (f" Ramp-up violations: { violations_up } " )
346+ print (f" Ramp-down violations: { violations_down } " )
347+ print (f" Max ramp change: { ramp_changes .max ():.2f} MW/h (limit: { ramp_up_limit :.2f} )" )
348+ print (f" Min ramp change: { ramp_changes .min ():.2f} MW/h (limit: -{ ramp_down_limit :.2f} )" )
349+
350+ # Show the specific violations
351+ violation_hours = ramp_changes [
352+ (ramp_changes > ramp_up_limit ) | (ramp_changes < - ramp_down_limit )
353+ ]
354+ print (f" Violation details:\n { violation_hours } " )
355+
356+ # ========================================================================
357+ # PLOTTING
358+ # ========================================================================
359+ fig , (ax1 , ax2 ) = plt .subplots (2 , 1 , figsize = (14 , 8 ), sharex = True )
360+
361+ # Dispatch
362+ ax1 .plot (dispatch .index , dispatch .values , 'b-' , linewidth = 2 , marker = 'o' , markersize = 3 )
363+ ax1 .set_ylabel ('Power (MW)' , fontsize = 12 )
364+ ax1 .set_title (f'{ unit } - Dispatch and Ramping' , fontsize = 14 , weight = 'bold' )
365+ ax1 .grid (True , alpha = 0.3 )
366+
367+ # Ramp changes with proper coloring
368+ colors = []
369+ for c in ramp_changes .values [1 :]: # Skip first NaN
370+ if abs (c ) > ramp_up_limit :
371+ colors .append ('red' )
372+ else :
373+ colors .append ('green' )
374+
375+ # Plot bars
376+ ax2 .bar (ramp_changes .index [1 :], ramp_changes .values [1 :],
377+ color = colors , alpha = 0.7 , edgecolor = 'black' , linewidth = 0.5 )
378+
379+ # Add limit lines
380+ ax2 .axhline (ramp_up_limit , color = 'darkred' , linestyle = '--' , linewidth = 2 ,
381+ label = f'Ramp-up limit ({ ramp_up_limit :.0f} MW/h)' )
382+ ax2 .axhline (- ramp_down_limit , color = 'darkred' , linestyle = '--' , linewidth = 2 ,
383+ label = f'Ramp-down limit ({ ramp_down_limit :.0f} MW/h)' )
384+ ax2 .axhline (0 , color = 'black' , linestyle = '-' , linewidth = 0.8 )
385+
386+ ax2 .set_xlabel ('Hour' , fontsize = 12 )
387+ ax2 .set_ylabel ('Ramp (MW/h)' , fontsize = 12 )
388+ ax2 .set_title ('Hour-to-Hour Changes' , fontsize = 12 )
389+ ax2 .legend (loc = 'upper right' )
390+ ax2 .grid (True , alpha = 0.3 , axis = 'y' )
391+
392+ # Add text annotation for violations
393+ if violations_up > 0 or violations_down > 0 :
394+ ax2 .text (0.02 , 0.98 ,
395+ f'⚠️ Violations: { violations_up + violations_down } ({ violations_up } up, { violations_down } down)' ,
396+ transform = ax2 .transAxes , fontsize = 11 , weight = 'bold' ,
397+ verticalalignment = 'top' ,
398+ bbox = dict (boxstyle = 'round' , facecolor = 'yellow' , alpha = 0.8 ))
399+ else :
400+ ax2 .text (0.02 , 0.98 ,
401+ '✓ All ramps within limits' ,
402+ transform = ax2 .transAxes , fontsize = 11 , weight = 'bold' ,
403+ verticalalignment = 'top' ,
404+ bbox = dict (boxstyle = 'round' , facecolor = 'lightgreen' , alpha = 0.8 ))
405+
406+ plt .tight_layout ()
407+ plt .savefig (f'{ output_folder } /{ unit } _ramping_check.png' ,
408+ dpi = 300 , bbox_inches = 'tight' )
409+ plt .show ()
410+
235411# for i in range(1,6):
236412# print(export_results[i][(export_results[i]['vartype'] == 'phydro') & (export_results[i]['unit'] == 'Barkley')]['value'].sum())
0 commit comments