Hydro-thermal power system planning problem: an introduction¶
The Brazilian interconnected power system have four regions, SE, S, N and NE, denoted by 0,1,2,3 for simiplicity. In each region, there are one integrated reserviour and several thermal plants to provide energy. Energy exchange is allowed between regions and an additional transshipment station. If demand can not be satisfied, a deficit cost will be incur. The objective is to minimize the total cost over the designing period meanwhile meeting energy requirements and feasibility constraints.
Notation¶
Formulation¶
Dynamics of each reservior \(i\) is given by
Thermal plant generated energy and the hydro generated energy are the sources to satisfy demand. Energy can be exchanged between reserviors. Energy deficit will incur if demand can’t be met. The supply-demand equation is thus
We assume there is no cost for hydro generation, \(\$u_k\) for every megawatt thermal plant \(k\) produces, \(\$v_{ij}\) for every megawatt deficit account produces. Objective is to minimize energy generation cost (in thousands) over 12 months
[1]:
import pandas
import numpy
import matplotlib.pyplot as plt
from msppy.msp import MSLP
from msppy.solver import Extensive
from msppy.solver import SDDP
from msppy.evaluation import Evaluation
import gurobipy
import seaborn
seaborn.set_style('darkgrid')
Data¶
[2]:
hydro_ = pandas.read_csv("./data/hydro.csv", index_col=0)
demand = pandas.read_csv("./data/demand.csv", index_col=0)
deficit_ = pandas.read_csv("./data/deficit.csv", index_col=0)
exchange_ub = pandas.read_csv("./data/exchange.csv", index_col=0)
exchange_cost = pandas.read_csv("./data/exchange_cost.csv", index_col=0)
thermal_ = [pandas.read_csv("./data/thermal_{}.csv".format(i), index_col=0) for i in range(4)]
The hydro_ dataframe gives the upper bounds of stored energy and hydro generation. It also gives the initial value of stored energy and inflow energy.
[3]:
hydro_
[3]:
| UB | INITIAL | |
|---|---|---|
| StoredEnergy_0 | 200717.6 | 59419.3000 |
| StoredEnergy_1 | 19617.2 | 5874.9000 |
| StoredEnergy_2 | 51806.1 | 12859.2000 |
| StoredEnergy_3 | 12744.9 | 5271.5000 |
| inflow_0 | 0.0 | 39717.5640 |
| inflow_1 | 0.0 | 6632.5141 |
| inflow_2 | 0.0 | 15897.1830 |
| inflow_3 | 0.0 | 2525.2938 |
| hydro_0 | 45414.3 | 0.0000 |
| hydro_1 | 13081.5 | 0.0000 |
| hydro_2 | 9900.9 | 0.0000 |
| hydro_3 | 7629.9 | 0.0000 |
thermal_ is a list containing four dataframes. Each dataframe provides LB, UB, Obj for each thermal plants in a specific region.
[4]:
thermal_[0].head()
[4]:
| LB | UB | OBJ | |
|---|---|---|---|
| 0 | |||
| 0 | 520.0 | 657 | 21.49 |
| 1 | 1080.0 | 1350 | 18.96 |
| 2 | 0.0 | 36 | 937.00 |
| 3 | 59.3 | 250 | 194.79 |
| 4 | 27.1 | 250 | 222.22 |
demand is a dataframe of monthly demand in each region. Demand is assumed to be deterministic.
[5]:
demand
[5]:
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| 0 | 45515 | 11692 | 10811 | 6507 |
| 1 | 46611 | 11933 | 10683 | 6564 |
| 2 | 47134 | 12005 | 10727 | 6506 |
| 3 | 46429 | 11478 | 10589 | 6556 |
| 4 | 45622 | 11145 | 10389 | 6645 |
| 5 | 45366 | 11146 | 10129 | 6669 |
| 6 | 45477 | 11055 | 10157 | 6627 |
| 7 | 46149 | 11051 | 10372 | 6772 |
| 8 | 46336 | 10917 | 10675 | 6843 |
| 9 | 46551 | 11015 | 10934 | 6815 |
| 10 | 46035 | 11156 | 11004 | 6871 |
| 11 | 45234 | 11297 | 10914 | 6701 |
deficit_ dataframe gives the maximum deficit energy each region can afford and its related costs. For example, to meet the demand for region 0, the maximum deficit energy each region can afford is 5%, 5%, 10%, 80% respectively.
[6]:
deficit_
[6]:
| OBJ | DEPTH | |
|---|---|---|
| 0 | 1142.80 | 0.05 |
| 1 | 2465.40 | 0.05 |
| 2 | 5152.46 | 0.10 |
| 3 | 5845.54 | 0.80 |
The exchange_ub dataframe gives the upper bound of energy flows between four regions (0,1,2,3) and one transshipment station (4). Number 99999999 indicates no upper limit. The exchange_cost dataframe gives related costs.
[7]:
exchange_ub
[7]:
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 0 | 0 | 7379 | 1000 | 0 | 4000 |
| 1 | 5625 | 0 | 0 | 0 | 0 |
| 2 | 600 | 0 | 0 | 0 | 2236 |
| 3 | 0 | 0 | 0 | 0 | 99999 |
| 4 | 3154 | 0 | 3951 | 3053 | 0 |
[8]:
exchange_cost
[8]:
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 0 | 0.0000 | 0.0010 | 0.0010 | 0.0010 | 0.0005 |
| 1 | 0.0010 | 0.0000 | 0.0010 | 0.0010 | 0.0005 |
| 2 | 0.0010 | 0.0010 | 0.0000 | 0.0010 | 0.0005 |
| 3 | 0.0010 | 0.0010 | 0.0010 | 0.0000 | 0.0005 |
| 4 | 0.0005 | 0.0005 | 0.0005 | 0.0005 | 0.0000 |
Inflow modelling¶
[9]:
hist = [pandas.read_csv("./data/hist_{}.csv".format(i), sep=";") for i in range(4)]
[10]:
hist[0].head()
[10]:
| YEAR | JAN | FEB | MAR | APR | MAY | JUN | JUL | AUG | SEP | OCT | NOV | DEC | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1931 | 56896.80 | 86488.31 | 88646.94 | 64581.71 | 43078.74 | 32150.18 | 25738.04 | 20606.05 | 22772.16 | 23767.55 | 25831.89 | 38566.50 |
| 1 | 1932 | 56451.95 | 61922.34 | 50742.10 | 35954.27 | 27000.91 | 25285.24 | 19913.20 | 16801.72 | 15034.40 | 22347.14 | 25004.76 | 48755.60 |
| 2 | 1933 | 65408.16 | 51128.33 | 40424.25 | 34627.46 | 24456.72 | 19099.70 | 16790.67 | 14192.07 | 13741.74 | 17339.43 | 18503.66 | 36070.84 |
| 3 | 1934 | 46580.39 | 37113.35 | 35392.72 | 27179.93 | 19080.88 | 14173.57 | 11860.90 | 9904.96 | 11835.56 | 12925.78 | 13594.84 | 33700.23 |
| 4 | 1935 | 54645.97 | 72711.15 | 61323.17 | 54644.39 | 34731.77 | 26184.86 | 19927.38 | 19833.32 | 18289.43 | 34902.46 | 26176.69 | 32033.65 |
The following plot is the first 200 months inflow data for reservior one. It clearly shows a seasonality pattern. While in this tutorial we will pretend scenarios are stage-wise independent discrete.
[11]:
plt.plot(hist[0].values.flatten()[0:200])
[11]:
[<matplotlib.lines.Line2D at 0x1a23376400>]
Concatenate the four dataframes and remove NA
[12]:
hist = pandas.concat(hist, axis=1)
hist.dropna(inplace=True)
hist.drop(columns='YEAR', inplace=True)
Each column of hist dataframe now gives scenarios of monthly inflow energy.
[13]:
hist.head()
[13]:
| JAN | FEB | MAR | APR | MAY | JUN | JUL | AUG | SEP | OCT | ... | MAR | APR | MAY | JUN | JUL | AUG | SEP | OCT | NOV | DEC | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 56896.80 | 86488.31 | 88646.94 | 64581.71 | 43078.74 | 32150.18 | 25738.04 | 20606.05 | 22772.16 | 23767.55 | ... | 23409.86 | 23382.44 | 10800.89 | 6555.49 | 4487.15 | 3223.12 | 2494.37 | 2569.03 | 4641.31 | 5928.45 |
| 1 | 56451.95 | 61922.34 | 50742.10 | 35954.27 | 27000.91 | 25285.24 | 19913.20 | 16801.72 | 15034.40 | 22347.14 | ... | 14320.59 | 10803.52 | 6621.21 | 4734.66 | 3104.44 | 2097.61 | 1673.39 | 2055.48 | 3710.95 | 6305.46 |
| 2 | 65408.16 | 51128.33 | 40424.25 | 34627.46 | 24456.72 | 19099.70 | 16790.67 | 14192.07 | 13741.74 | 17339.43 | ... | 13800.87 | 14378.16 | 9859.45 | 5247.05 | 3383.62 | 2428.81 | 1773.43 | 1617.70 | 4108.02 | 7741.12 |
| 3 | 46580.39 | 37113.35 | 35392.72 | 27179.93 | 19080.88 | 14173.57 | 11860.90 | 9904.96 | 11835.56 | 12925.78 | ... | 14352.06 | 13302.03 | 10134.26 | 5947.45 | 3729.34 | 2481.26 | 1469.26 | 1797.54 | 2322.72 | 5572.98 |
| 4 | 54645.97 | 72711.15 | 61323.17 | 54644.39 | 34731.77 | 26184.86 | 19927.38 | 19833.32 | 18289.43 | 34902.46 | ... | 18872.78 | 23222.27 | 15783.48 | 8962.09 | 5208.33 | 3864.63 | 2816.89 | 2790.11 | 4112.86 | 7840.01 |
5 rows × 48 columns
Disjoin scenarios for each regions.
[14]:
scenarios = [hist.iloc[:,12*i:12*(i+1)].transpose().values for i in range(4)]
Solution¶
[15]:
T = 3
[16]:
HydroThermal = MSLP(T=T, bound=0, discount=0.9906)
for t in range(T):
m = HydroThermal[t]
stored_now, stored_past = m.addStateVars(4, ub=hydro_['UB'][:4], name="stored")
spill = m.addVars(4, name="spill", obj=0.001)
hydro = m.addVars(4, ub=hydro_['UB'][-4:], name="hydro")
deficit = m.addVars(
[(i,j) for i in range(4) for j in range(4)],
ub = [demand.iloc[t%12][i] * deficit_['DEPTH'][j] for i in range(4) for j in range(4)],
obj = [deficit_['OBJ'][j] for i in range(4) for j in range(4)],
name = "deficit")
thermal = [None] * 4
for i in range(4):
thermal[i] = m.addVars(
len(thermal_[i]),
ub=thermal_[i]['UB'],
lb=thermal_[i]['LB'],
obj=thermal_[i]['OBJ'],
name="thermal_{}".format(i)
)
exchange = m.addVars(5,5, obj=exchange_cost.values.flatten(),
ub=exchange_ub.values.flatten(), name="exchange")
thermal_sum = m.addVars(4, name="thermal_sum")
m.addConstrs(thermal_sum[i] == gurobipy.quicksum(thermal[i].values()) for i in range(4))
for i in range(4):
m.addConstr(
thermal_sum[i]
+ gurobipy.quicksum(deficit[(i,j)] for j in range(4))
+ hydro[i]
- gurobipy.quicksum(exchange[(i,j)] for j in range(5))
+ gurobipy.quicksum(exchange[(j,i)] for j in range(5))
== demand.iloc[t%12][i]
)
m.addConstr(
gurobipy.quicksum(exchange[(j,4)] for j in range(5))
- gurobipy.quicksum(exchange[(4,j)] for j in range(5))
== 0
)
for i in range(4):
if t == 0:
m.addConstr(
stored_now[i] + spill[i] + hydro[i] - stored_past[i]
== hydro_['INITIAL'][4:8][i]
)
else:
m.addConstr(
stored_now[i] + spill[i] + hydro[i] - stored_past[i] == 0,
uncertainty={'rhs': scenarios[i][t%12]}
)
if t == 0:
m.addConstrs(stored_past[i] == hydro_['INITIAL'][:4][i] for i in range(4))
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
[17]:
# Use the extensive solver
Extensive(HydroThermal).solve(outputFlag=0)
Academic license - for non-commercial use only
[17]:
782309.1877977113
[18]:
# Use the SDDP solver
HydroThermal_SDDP = SDDP(HydroThermal)
HydroThermal_SDDP.solve(logFile=0, n_processes=3, logToConsole=0, max_iterations=300)
[19]:
HydroThermal_SDDP.plot_bounds();
We now evaluate the policy by computing the policy values exhaustively for every scenarios. Suppose we are interested in solutions of hydro-generation.
[20]:
result = Evaluation(HydroThermal)
result.run(n_simulations=-1, query = ['hydro[{}]'.format(i) for i in range(4)])
%matplotlib inline
result.solution['hydro[0]'].mean().plot(legend = False)
[20]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a23705e48>
epv is the exact value of the expected policy value. pv is the list of computed policy values.
[22]:
result.gap, result.epv, numpy.std(result.pv)
[22]:
(4.047462777712249e-07, 782309.0736226843, 96882.62071229359)