@@ -144,6 +144,9 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
144144)
145145
146146inputs .load_and_check_data ()
147+ print ("\n === Hydro Ramping Rates ===" )
148+ for unit in inputs .hydro_units :
149+ print (f"{ unit :20s} | RU: { inputs .hydro_RU [unit ]:6.1f} MW/h | RD: { inputs .hydro_RD [unit ]:6.1f} MW/h" )
147150
148151# Dictionaries to store results and models
149152power_system_model = {}
@@ -291,122 +294,171 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
291294
292295import matplotlib .pyplot as plt
293296
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 )
297+ # =========================
298+ # Hydro ramping check + plot
299+ # =========================
339300
340- violations_up = (ramp_changes > ramp_up_limit ).sum ()
341- violations_down = (ramp_changes < - ramp_down_limit ).sum ()
301+ EPS = 1e-3 # tolerance for float noise; adjust to 1e-5 if needed
342302
343- if violations_up > 0 or violations_down > 0 :
303+ def plot_hydro_ramping_check (
304+ hydro_data : pd .DataFrame ,
305+ hydro_RU : dict ,
306+ hydro_RD : dict ,
307+ output_folder : str ,
308+ units_to_plot = None ,
309+ eps : float = EPS ,
310+ ):
311+ """
312+ hydro_data: long dataframe with columns ['hour','unit','value'] for vartype='phydro'
313+ hydro_RU / hydro_RD: dict[unit] -> MW/h
314+ """
315+ if hydro_data .empty :
316+ print ("No hydro dispatch to check." )
317+ return
318+
319+ # --- Clean hour, ensure numeric, sort ---
320+ df = hydro_data .copy ()
321+ df ["hour" ] = pd .to_numeric (df ["hour" ], errors = "coerce" )
322+ df = df .dropna (subset = ["hour" ])
323+ df ["hour" ] = df ["hour" ].astype (int )
324+ df = df .sort_values (["unit" , "hour" ])
325+
326+ # --- Pivot: hour index, unit columns ---
327+ pivot = df .pivot (index = "hour" , columns = "unit" , values = "value" ).sort_index ()
328+
329+ # Validate index integrity
330+ if pivot .index .has_duplicates :
331+ dupes = pivot .index [pivot .index .duplicated ()].unique ().tolist ()
332+ raise ValueError (f"Duplicate hours in hydro pivot index: { dupes [:20 ]} ..." )
333+
334+ # Choose units
335+ units = list (pivot .columns ) if units_to_plot is None else [u for u in units_to_plot if u in pivot .columns ]
336+ if not units :
337+ print ("No matching hydro units to plot." )
338+ return
339+
340+ os .makedirs (output_folder , exist_ok = True )
341+
342+ for unit in units :
343+ dispatch = pivot [unit ].dropna ().sort_index ()
344+ if dispatch .empty or len (dispatch ) < 2 :
345+ print (f"{ unit } : not enough data to compute ramps." )
346+ continue
347+
348+ ru = float (hydro_RU .get (unit , np .inf ))
349+ rd = float (hydro_RD .get (unit , np .inf ))
350+
351+ ramps = dispatch .diff () # MW/h; first is NaN
352+ ramps_no_na = ramps .dropna ()
353+
354+ pen = float (getattr (inputs , "hydro_ramp_penalty" , {}).get (unit , 0.0 ))
355+ ramp_cost_series = pen * ramps_no_na .abs ()
356+ total_ramp_cost = float (ramp_cost_series .sum ())
357+
358+ # --- Violation logic with tolerance ---
359+ # ramp-up violation if change > RU + eps
360+ # ramp-down violation if change < -RD - eps
361+ viol_up_mask = ramps_no_na > (ru + eps )
362+ viol_dn_mask = ramps_no_na < (- rd - eps )
363+ viol_mask = viol_up_mask | viol_dn_mask
364+
365+ n_up = int (viol_up_mask .sum ())
366+ n_dn = int (viol_dn_mask .sum ())
367+
368+ # --- Diagnostics: show exact float residuals for "near-limit" issues ---
369+ max_ramp = float (ramps_no_na .max ())
370+ min_ramp = float (ramps_no_na .min ())
371+
372+ if (n_up > 0 ) or (n_dn > 0 ):
344373 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} )" )
374+ print (f" Ramp-up violations: { n_up } " )
375+ print (f" Ramp-down violations: { n_dn } " )
376+ print (f" Max ramp change: { max_ramp :.12f} MW/h (limit: { ru :.12f} )" )
377+ print (f" Min ramp change: { min_ramp :.12f} MW/h (limit: { - rd :.12f} )" )
378+ print (" Violation details (full precision):" )
379+ viol_series = ramps_no_na [viol_mask ].copy ()
380+ # show residual above RU for up, below -RD for down
381+ resid = pd .Series (index = viol_series .index , dtype = float )
382+ resid .loc [viol_up_mask [viol_mask ].index ] = viol_series .loc [viol_up_mask ] - ru
383+ resid .loc [viol_dn_mask [viol_mask ].index ] = (- rd ) - viol_series .loc [viol_dn_mask ] # positive means beyond limit
384+ out = pd .DataFrame ({"ramp" : viol_series , "residual" : resid })
385+ pd .set_option ("display.precision" , 15 )
386+ print (out )
387+ print (f" Ramping cost (${ pen } /MW): { total_ramp_cost :.2f} " )
388+ else :
389+ print (f"\n ✅ { unit } : all ramps within limits (eps={ eps } )." )
390+ print (f" Max ramp change: { max_ramp :.12f} MW/h (limit: { ru :.12f} )" )
391+ print (f" Min ramp change: { min_ramp :.12f} MW/h (limit: { - rd :.12f} )" )
392+ print (f" Ramping cost (${ pen } /MW): { total_ramp_cost :.2f} " )
349393
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 } " )
394+ # --- Colors for bars (correct, sign-aware) ---
395+ colors = []
396+ for h , c in ramps_no_na .items ():
397+ if c > ru + eps :
398+ colors .append ("red" )
399+ elif c < - rd - eps :
400+ colors .append ("red" )
401+ else :
402+ colors .append ("green" )
355403
356- # ========================================================================
357- # PLOTTING
358- # ========================================================================
404+ # --- Plot ---
359405 fig , (ax1 , ax2 ) = plt .subplots (2 , 1 , figsize = (14 , 8 ), sharex = True )
360406
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' )
407+ # Dispatch plot
408+ ax1 .plot (dispatch .index , dispatch .values , "-o" , linewidth = 2 , markersize = 3 , color = "blue" )
409+ ax1 .set_ylabel (" Power (MW)" )
410+ ax1 .set_title (f" { unit } - Dispatch and Ramping Check" )
365411 ax1 .grid (True , alpha = 0.3 )
366412
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 ))
413+ # Ramp bars
414+ ax2 .bar (ramps_no_na .index , ramps_no_na .values , color = colors , alpha = 0.75 , edgecolor = "black" , linewidth = 0.4 )
415+ ax2 .axhline (ru , color = "darkred" , linestyle = "--" , linewidth = 2 , label = f"Ramp-up limit (+{ ru :g} MW/h)" )
416+ ax2 .axhline (- rd , color = "darkred" , linestyle = "--" , linewidth = 2 , label = f"Ramp-down limit (-{ rd :g} MW/h)" )
417+ ax2 .axhline (0 , color = "black" , linewidth = 0.8 )
418+
419+ ax2 .set_xlabel ("Hour" )
420+ ax2 .set_ylabel ("Ramp (MW/h)" )
421+ ax2 .grid (True , alpha = 0.3 , axis = "y" )
422+ ax2 .legend (loc = "upper right" )
423+
424+ # Annotation
425+ if n_up + n_dn > 0 :
426+ ax2 .text (
427+ 0.02 , 0.98 ,
428+ f"Violations: { n_up + n_dn } ({ n_up } up, { n_dn } down)" ,
429+ transform = ax2 .transAxes ,
430+ va = "top" ,
431+ fontsize = 11 ,
432+ bbox = dict (boxstyle = "round" , facecolor = "yellow" , alpha = 0.85 ),
433+ )
399434 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 ))
435+ ax2 .text (
436+ 0.02 , 0.98 ,
437+ "All ramps within limits" ,
438+ transform = ax2 .transAxes ,
439+ va = "top" ,
440+ fontsize = 11 ,
441+ bbox = dict (boxstyle = "round" , facecolor = "lightgreen" , alpha = 0.85 ),
442+ )
405443
406444 plt .tight_layout ()
407- plt . savefig ( f' { output_folder } / { unit } _ramping_check.png' ,
408- dpi = 300 , bbox_inches = ' tight' )
445+ fname = os . path . join ( output_folder , f" { unit } _ramping_check.png" )
446+ plt . savefig ( fname , dpi = 300 , bbox_inches = " tight" )
409447 plt .show ()
410448
449+ # final_day_results should be a dataframe with columns: ['hour','vartype','unit','node','value']
450+ final_day_results = export_results [start_day + steps_to_run - 2 ]
451+
452+ hydro_data = final_day_results .loc [final_day_results ["vartype" ] == "phydro" , ["hour" , "unit" , "value" ]]
453+
454+ plot_hydro_ramping_check (
455+ hydro_data = hydro_data ,
456+ hydro_RU = inputs .hydro_RU ,
457+ hydro_RD = inputs .hydro_RD ,
458+ output_folder = output_folder ,
459+ units_to_plot = None , # or e.g. ["J_Percy_Priest"]
460+ eps = 1e-6 ,
461+ )
462+
411463# for i in range(1,6):
412464# print(export_results[i][(export_results[i]['vartype'] == 'phydro') & (export_results[i]['unit'] == 'Barkley')]['value'].sum())
0 commit comments