Skip to content

Commit 68e20a1

Browse files
committed
fix(ramping): reads all the static parameters from nondispatch_unit.csv, introduced ramping penalty
1 parent 39684fe commit 68e20a1

File tree

3 files changed

+374
-158
lines changed

3 files changed

+374
-158
lines changed

scripts/run_hydrochipps.py

Lines changed: 155 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
144144
)
145145

146146
inputs.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
149152
power_system_model = {}
@@ -291,122 +294,171 @@ def update_contract_costs(inputs, supplier, new_values, start_day):
291294

292295
import 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

Comments
 (0)