"""
EELT 7030 — Operation and Expansion Planning of Electric Power Systems
Federal University of Paraná (UFPR)
Utility: Post-Solve Dispatch Summary and Cost Reports
Author
------
Augusto Mathias Adams <augusto.adams@ufpr.br>
Description
-----------
This module contains functions to summarize dispatch results after solving
a Pyomo-based optimization problem. It prints total generation, cost breakdowns,
and unit-level summaries for hydropower, thermal, renewable, and storage technologies.
Features:
- Total cost, demand, deficit and thermal cost components.
- Per-unit dispatch summaries with color-enhanced output (via `colorama`).
- Compatible with modular NaivePyDESSEM subsystem architecture.
References
----------
[1] CEPEL, DESSEM. Manual de Metodologia, 2023
[2] Unsihuay Vila, C. Introdução aos Sistemas de Energia Elétrica, Lecture Notes, EELT7030/UFPR, 2023.
"""
from pyomo.environ import ConcreteModel, value
from typing import Any, Tuple, Dict
from colorama import Fore, Style
from .Formatters import format_brl
from .ModelCheck import *
def __compute_total_generation(model: ConcreteModel) -> float:
"""
Compute total system generation across all technologies.
Parameters
----------
model : ConcreteModel
Solved Pyomo model instance.
Returns
-------
float
Total generated energy [MWh] across hydro, thermal, renewable, and storage.
"""
total_generation = 0.0
if has_generator_model(model):
total_generation += sum(
value(model.level_hours[p] * model.gen_P[g, t, p])
for g in model.GU for t in model.T for p in model.P
)
if has_storage_model(model):
total_generation += sum(value(
model.level_hours[p] * (
model.storage_dis[s, t, p] - model.storage_ch[s, t, p]
))
for s in model.SU for t in model.T for p in model.P
)
return total_generation
def __compute_investment_cost(model: ConcreteModel) -> float:
"""
Compute total investment cost.
Parameters
----------
model : ConcreteModel
Solved Pyomo model.
Returns
-------
float
Total investment[$] for building units (geerators and storages).
"""
total_investment = 0.0
if has_generator_model(model):
if model.parcel_investment:
total_investment += sum(model.discounts[t] * value(model.gen_c_inv[g] * model.gen_x[g, t])
for g in model.GU for t in model.T)
else:
total_investment += sum(value(model.discounts[t] * model.gen_c_inv[g] * model.gen_y[g, t])
for g in model.GU for t in model.T)
if has_storage_model(model):
if model.parcel_investment:
total_investment += sum(value(
model.discounts[t] * model.storage_c_inv[s] * model.storage_x[s, t]) for s in model.SU for t in model.T)
else:
total_investment += sum(value(
model.discounts[t] * model.storage_c_inv[s] * model.storage_y[s, t]) for s in model.SU for t in model.T)
if has_transmission_line_model(model):
if model.parcel_investment:
total_investment += sum(value(
model.discounts[t] * model.lines_c_inv[l] * model.lines_x[l, t]
)
for l in model.LT for t in model.T)
else:
total_investment += sum(value(
model.discounts[t] * model.lines_c_inv[l] * model.lines_y[l, t]
)
for l in model.LT for t in model.T)
return total_investment
def __compute_operational_cost(model: ConcreteModel) -> float:
"""
Compute total operational cost.
Parameters
----------
model : ConcreteModel
Solved Pyomo model.
Returns
-------
float
Total cost[$] for operating units (geerators and storages).
"""
total_op_cost = 0.0
if has_generator_model(model):
total_op_cost += sum(
value(model.discounts[t] * model.level_hours[p] *
model.gen_c_op[g] * model.gen_P[g, t, p]
)
for g in model.GU for t in model.T for p in model.P
)
if has_storage_model(model):
total_op_cost += sum(
value(model.discounts[t] * model.level_hours[p] * model.storage_c_op[s] * (
model.storage_ch[s, t, p] + model.storage_dis[s, t, p]
))
for s in model.SU for t in model.T for p in model.P
)
if has_transmission_line_model(model):
total_op_cost += sum(
value(model.discounts[t] * model.level_hours[p] * model.lines_c_op[l] * model.lines_flow[l, t, p]
)
for l in model.LT for t in model.T for p in model.P
)
if has_connection_bar_model(model):
total_op_cost += sum(
value(model.discounts[t] * model.level_hours[p] * model.Cdef[b] * model.D[b, t, p]
)
for b in model.CB for t in model.T for p in model.P)
return total_op_cost
[docs]
def dispatch_summary(model: ConcreteModel) -> None:
"""
Print a complete dispatch and cost summary including:
- Total generation and demand.
- Deficit and its monetary cost.
- Thermal cost breakdown (start-up, generation).
- Overall total cost from the model objective.
Parameters
----------
model : ConcreteModel
Solved Pyomo model instance.
"""
print(f"\n{Fore.MAGENTA}{Style.BRIGHT}==================== EXPANSION SUMMARY ===================={Style.RESET_ALL}")
total_generation = __compute_total_generation(model)
print(f" {Fore.CYAN}Total Generation{Style.RESET_ALL}: {Fore.RED}{total_generation:.2f} MWh")
demand = sum(value(model.level_hours[p] * model.d[b, t, p])
for b in model.CB for t in model.T for p in model.P)
print(
f" {Fore.CYAN}Total Demand{Style.RESET_ALL}: {Fore.RED}{demand:.2f} MWh")
print(
f" {Fore.CYAN}Total Investment Cost{Style.RESET_ALL}: {Fore.RED} $ {format_brl(__compute_investment_cost(model))}")
print(
f" {Fore.CYAN}Total Operational Cost{Style.RESET_ALL}: {Fore.RED} $ {format_brl(__compute_operational_cost(model))}")
[docs]
def generator_dispatch_summary(model: ConcreteModel) -> None:
"""
Print unit-level generator units generation summary in MWh.
Parameters
----------
model : ConcreteModel
Solved Pyomo model with generator subsystem.
"""
if has_generator_model(model):
print(f"{Fore.YELLOW}Generator Units Generation:{Style.RESET_ALL}")
for g in model.GU:
# Identify line state
if value(model.gen_x[g, 1]) >= 0.5:
state = "Existing"
entry_time = 1
else:
state = "Candidate"
# find first t where y[l,t] = 1
entry_time = next(
(t for t in model.T if hasattr(model, "gen_y") and value(model.gen_y[g, t]) > 0.5),
None
)
dispatch = sum(value(
model.level_hours[p] * model.gen_P[g, t, p]) for t in model.T for p in model.P)
print(
f" {Fore.BLUE}{g} ({state}){Style.RESET_ALL}: {Fore.RED}{dispatch:.2f} MWh")
if entry_time:
print(f" {Fore.BLUE}Entry Period (t*):{Style.RESET_ALL} {Fore.MAGENTA}{entry_time}{Style.RESET_ALL}")
else:
print(f" {Fore.BLUE}Entry Period (t*):{Style.RESET_ALL} {Fore.MAGENTA}— never built —{Style.RESET_ALL}")
[docs]
def storage_dispatch_summary(model: ConcreteModel) -> None:
"""
Print unit-level storage discharge summary in MWh.
Parameters
----------
model : ConcreteModel
Solved Pyomo model with storage subsystem.
"""
if has_storage_model(model):
print(f"\n{Fore.YELLOW}Storage Discharge Summary: {Style.RESET_ALL}")
for s in model.SU:
# Identify line state
if value(model.storage_x[s, 1]) >= 0.5:
state = "Existing"
entry_time = 1
else:
state = "Candidate"
# find first t where y[l,t] = 1
entry_time = next(
(t for t in model.T if hasattr(model, "storage_y") and value(model.storage_y[s, t]) > 0.5),
None
)
dispatch = sum(value(
model.storage_dis[s, t, p] * model.level_hours[p]) for t in model.T for p in model.P)
print(
f" {Fore.MAGENTA}Storage {s} ({state}) - Discharge{Style.RESET_ALL}: {Fore.RED}{dispatch:.2f} MWh")
dispatch = sum(value(
model.storage_ch[s, t, p] * model.level_hours[p]) for t in model.T for p in model.P)
print(
f" {Fore.MAGENTA}{s} - Charge{Style.RESET_ALL}: {Fore.RED}{dispatch:.2f} MWh")
dispatch = sum(
value((model.storage_dis[s, t, p] - model.storage_ch[s, t, p]) * model.level_hours[p]) for t in model.T for p in model.P)
print(
f" {Fore.MAGENTA}{s} - Delta{Style.RESET_ALL}: {Fore.RED}{dispatch:.2f} MWh")
if entry_time:
print(f" {Fore.BLUE}Entry Period (t*):{Style.RESET_ALL} {Fore.MAGENTA}{entry_time}{Style.RESET_ALL}")
else:
print(f" {Fore.BLUE}Entry Period (t*):{Style.RESET_ALL} {Fore.MAGENTA}— never built —{Style.RESET_ALL}")
[docs]
def connection_bar_dispatch_summary(model: ConcreteModel) -> None:
"""
Print a unit-level dispatch summary for each connection bar in MWh.
Parameters
----------
model : pyomo.environ.ConcreteModel
A solved Pyomo model containing a valid connection-bar subsystem,
verified via :func:`has_connection_bar_model`.
Returns
-------
None
Prints formatted results directly to the console.
"""
if has_connection_bar_model(model):
if not model.unique_bar:
print(f"\n{Fore.YELLOW}Connection Bar Summary Dispatch:{Style.RESET_ALL}")
for b in model.CB:
total_demand = sum(value(model.d[b, t, p] * model.level_hours[p]) for t in model.T for p in model.P)
total_deficit = sum(value(model.D[b, t, p] * model.level_hours[p]) for t in model.T for p in model.P)
served = total_demand - total_deficit
avg_angle = 0.0
if not model.unique_bar:
avg_angle = sum(value(model.theta[b, t, p])
for t in model.T for p in model.P) / (len(model.T) + len(model.P))
print(f"\n{Fore.CYAN}Bar {b}:{Style.RESET_ALL}")
print(
f" {Fore.BLUE}Demand:{Style.RESET_ALL} {Fore.RED}{total_demand:.2f} MWh{Style.RESET_ALL}")
print(
f" {Fore.GREEN}Served:{Style.RESET_ALL} {Fore.RED}{served:.2f} MWh{Style.RESET_ALL}")
print(
f" {Fore.MAGENTA}Deficit:{Style.RESET_ALL} {Fore.RED}{total_deficit:.2f} MWh{Style.RESET_ALL}")
print(
f" {Fore.YELLOW}Avg θ:{Style.RESET_ALL} {Fore.RED}{avg_angle:.4f} rad{Style.RESET_ALL}")
[docs]
def transmission_line_dispatch_summary(model: ConcreteModel) -> None:
"""
Print a unit-level dispatch summary for each transmission line in MWh.
Parameters
----------
model : pyomo.environ.ConcreteModel
A solved Pyomo model containing a valid transmission line subsystem,
verified via :func:`has_transmission_line_model`.
Returns
-------
None
Prints formatted results directly to the console.
"""
if has_transmission_line_model(model):
print(f"\n{Fore.YELLOW}Transmission Line Summary Dispatch:{Style.RESET_ALL}")
for l in model.LT:
# Identify line state
if value(model.lines_x[l, 1]) >= 0.5:
state = "Existing"
entry_time = 1
else:
state = "Candidate"
# find first t where y[l,t] = 1
entry_time = next(
(t for t in model.T if hasattr(model, "lines_y") and value(model.lines_y[l, t]) > 0.5),
None
)
total_flow = sum(abs(value(model.lines_flow[l, t, p]) * model.level_hours[p]) for t in model.T for p in model.P)
print(f"\n{Fore.CYAN}Line {l} ({state}):{Style.RESET_ALL}")
print(
f" {Fore.BLUE}Power Flow: {Style.RESET_ALL} {Fore.RED}{total_flow:.2f} MWh{Style.RESET_ALL}")
if entry_time:
print(f" {Fore.BLUE}Entry Period (t*):{Style.RESET_ALL} {Fore.MAGENTA}{entry_time}{Style.RESET_ALL}")
else:
print(f" {Fore.BLUE}Entry Period (t*):{Style.RESET_ALL} {Fore.MAGENTA}— never built —{Style.RESET_ALL}")