Solve the hydro-thermal power system planning problem: periodical SDDP¶
[1]:
import pandas
import numpy
import gurobipy
from msppy.msp import MSLP
from msppy.solver import PSDDP
from msppy.evaluation import EvaluationTrue, Evaluation
import sys
gamma = numpy.array(pandas.read_csv(
"./data/gamma.csv",
names=[0,1,2,3],
index_col=0,
skiprows=1,
))
sigma = [
numpy.array(pandas.read_csv(
"./data/sigma_{}.csv".format(i),
names=[0,1,2,3],
index_col=0,
skiprows=1,
)) for i in range(12)
]
exp_mu = numpy.array(pandas.read_csv(
"./data/exp_mu.csv",
names=[0,1,2,3],
index_col=0,
skiprows=1,
))
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)]
stored_initial = hydro_['INITIAL'][:4]
inflow_initial = hydro_['INITIAL'][4:8]
def sampler(t):
def inner(random_state):
noise = numpy.exp(
random_state.multivariate_normal(mean=[0]*4, cov=sigma[t%12]))
coef = [None]*4
rhs = [None]*4
for i in range(4):
coef[i] = -noise[i]*gamma[t%12][i]*exp_mu[t%12][i]/exp_mu[(t-1)%12][i]
rhs[i] = noise[i]*(1-gamma[t%12][i])*exp_mu[t%12][i]
return (coef+rhs)
return inner
Build the true problem and make discretization¶
[2]:
HydroThermal = MSLP(T=13, bound=0, discount=0.9906)
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Periodical SDDP algorithm solves the problem for a single period plus an initial stage. In this case, the number of stages to consider is 13, set by \(T=13\).
[3]:
for t in range(13):
m = HydroThermal[t]
stored_now,stored_past = m.addStateVars(4, ub=hydro_['UB'][:4], name="stored")
inflow_now,inflow_past = m.addStateVars(4, name="inflow")
spill = m.addVars(4, obj=0.001, name="spill")
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],
name = 'demand',
)
m.addConstr(
gurobipy.quicksum(exchange[(j,4)] for j in range(5))
- gurobipy.quicksum(exchange[(4,j)] for j in range(5))
== 0
)
m.addConstrs(
stored_now[i] + spill[i] + hydro[i] - stored_past[i] == inflow_now[i]
for i in range(4)
)
if t == 0:
m.addConstrs(stored_past[i] == stored_initial[i] for i in range(4))
m.addConstrs(inflow_now[i] == inflow_initial[i] for i in range(4))
else:
TS = m.addConstrs(inflow_now[i] + inflow_past[i] == 0 for i in range(4))
m.add_continuous_uncertainty(
uncertainty=sampler(t),
locations=(
[(TS[i],inflow_past[i]) for i in range(4)]
+ [TS[i] for i in range(4)]
),
)
HydroThermal.discretize(n_samples=100, random_state=888)
Solve the problem¶
We now call PSDDP solver to run the periodical SDDP for 20 iterations.
Backward passes of the periodical SDDP generates cuts for the first 13 stages.
Forward passes of the periodical SDDP generates trial points. Trial points can be just obtained from solving these 13 stages. They can also be obtained from later stages (since the problem is periodical). It is often found solving more stages makes the algorithm converge faster. Here we set \(\textrm{forward_T}=120\), meaning that trial points are generated from the first 120 stages.
[4]:
HT_psddp = PSDDP(HydroThermal)
HT_psddp.solve(max_iterations=10, forward_T=120)
----------------------------------------------------------------
SDDP Solver, Lingquan Ding
----------------------------------------------------------------
Iteration Bound Value Time
----------------------------------------------------------------
1 245082.919600 1734139943.585172 0.426226
2 275523.302075 396460564.856688 0.773433
3 3605638.658380 343399590.174994 0.456558
4 7472497.857329 591001246.721298 0.357867
5 7472497.857329 175229142.726806 0.365556
6 8608579.050937 455731628.093480 0.337117
7 18568417.721664 316499896.612110 0.328819
8 18568417.721664 252349984.539381 0.382036
9 18568417.721664 1034789138.913040 0.362973
10 29642949.251290 483878406.759454 0.357292
----------------------------------------------------------------
Time: 4.147876501083374 seconds
Algorithm stops since iteration:10 has reached
Evaluate the obtained policy¶
The obtained policy is implementable feasible for any finite number of stages. We can for example, set \(\textrm{query}_T=60}\) as below to evaluate the policy for the first 60 stages.
[5]:
result = Evaluation(HydroThermal)
result.run(n_simulations=10, query_T=60)
result.CI
[5]:
(199193170.50824797, 406535057.5869812)