|
| 1 | +# Copyright 2015, Vincent Leclere, Francois Pacaud and Henri Gerard |
| 2 | +# This Source Code Form is subject to the terms of the Mozilla Public |
| 3 | +# License, v. 2.0. If a copy of the MPL was not distributed with this |
| 4 | +# file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| 5 | +############################################################################# |
| 6 | +# Benchmark SDDP and DP algorithms upon damsvalley example |
| 7 | +# This benchmark includes: |
| 8 | +# - comparison of execution time |
| 9 | +# - gap between the stochastic solution and the anticipative solution |
| 10 | +# |
| 11 | +# Usage: |
| 12 | +# ``` |
| 13 | +# $ julia benchmark.jl {DP|SDDP} |
| 14 | +# |
| 15 | +# ``` |
| 16 | +############################################################################# |
| 17 | + |
| 18 | +srand(2713) |
| 19 | +push!(LOAD_PATH, "../src") |
| 20 | + |
| 21 | +using StochDynamicProgramming, JuMP |
| 22 | +using Clp |
| 23 | + |
| 24 | +SOLVER = ClpSolver() |
| 25 | +#= const SOLVER = CplexSolver(CPX_PARAM_SIMDISPLAY=0, CPX_PARAM_THREADS=4) =# |
| 26 | + |
| 27 | +const N_STAGES = 10 |
| 28 | + |
| 29 | +# COST: |
| 30 | +const COST = -66*2.7*(1 + .5*(rand(N_STAGES) - .5)) |
| 31 | + |
| 32 | +# Define dynamic of the dam: |
| 33 | +function dynamic(t, x, u, w) |
| 34 | + return [x[1] - u[1] - u[3] + w[1], x[2] - u[2] - u[4] + u[1] + u[3]] |
| 35 | +end |
| 36 | + |
| 37 | +# Define cost corresponding to each timestep: |
| 38 | +function cost_t(t, x, u, w) |
| 39 | + return COST[t] * (u[1] + u[2]) |
| 40 | +end |
| 41 | + |
| 42 | +function final_cost(x) |
| 43 | + return 0. |
| 44 | +end |
| 45 | + |
| 46 | + |
| 47 | + |
| 48 | +"""Solve the problem with a solver, supposing the aleas are known |
| 49 | +in advance.""" |
| 50 | +function solve_anticipative_problem(model, scenario) |
| 51 | + m = Model(solver=SOLVER) |
| 52 | + |
| 53 | + |
| 54 | + @defVar(m, model.xlim[1][1] <= x1[1:(N_STAGES)] <= model.xlim[1][2]) |
| 55 | + @defVar(m, model.xlim[2][1] <= x2[1:(N_STAGES)] <= model.xlim[2][2]) |
| 56 | + @defVar(m, model.ulim[1][1] <= u1[1:N_STAGES-1] <= model.ulim[1][2]) |
| 57 | + @defVar(m, model.ulim[2][1] <= u2[1:N_STAGES-1] <= model.ulim[2][2]) |
| 58 | + |
| 59 | + @setObjective(m, Min, sum{COST[i]*(u1[i] + u2[i]), i = 1:N_STAGES-1}) |
| 60 | + |
| 61 | + for i in 1:N_STAGES-1 |
| 62 | + @addConstraint(m, x1[i+1] - x1[i] + u1[i] - scenario[i] == 0) |
| 63 | + @addConstraint(m, x2[i+1] - x2[i] + u2[i] - u1[i] == 0) |
| 64 | + end |
| 65 | + |
| 66 | + @addConstraint(m, x1[1] == model.initialState[1]) |
| 67 | + @addConstraint(m, x2[1] == model.initialState[2]) |
| 68 | + |
| 69 | + status = solve(m) |
| 70 | + return getObjectiveValue(m) |
| 71 | +end |
| 72 | + |
| 73 | + |
| 74 | +"""Build aleas probabilities for each week. |
| 75 | +
|
| 76 | +Return an array with size (N_ALEAS, N_STAGES) |
| 77 | +
|
| 78 | +""" |
| 79 | +function build_aleas() |
| 80 | + W_MAX = round(Int, .5/7. * 100) |
| 81 | + W_MIN = 0 |
| 82 | + DW = 1 |
| 83 | + # Define aleas' space: |
| 84 | + N_ALEAS = Int(round(Int, (W_MAX - W_MIN) / DW + 1)) |
| 85 | + ALEAS = linspace(W_MIN, W_MAX, N_ALEAS) |
| 86 | + |
| 87 | + aleas = zeros(N_ALEAS, N_STAGES) |
| 88 | + |
| 89 | + # take into account seasonality effects: |
| 90 | + unorm_prob = linspace(1, N_ALEAS, N_ALEAS) |
| 91 | + proba1 = unorm_prob / sum(unorm_prob) |
| 92 | + proba2 = proba1[N_ALEAS:-1:1] |
| 93 | + |
| 94 | + for t in 1:N_STAGES |
| 95 | + aleas[:, t] = (1 - sin(pi*t/N_STAGES)) * proba1 + sin(pi*t/N_STAGES) * proba2 |
| 96 | + end |
| 97 | + return aleas |
| 98 | +end |
| 99 | + |
| 100 | + |
| 101 | +"""Build an admissible scenario for water inflow. |
| 102 | +
|
| 103 | +Parameters: |
| 104 | +- n_scenarios (Int64) |
| 105 | + Number of scenarios to generate |
| 106 | +
|
| 107 | +- probabilities (Array{Float64, 2}) |
| 108 | + Probabilities of occurence of water inflow for each week |
| 109 | +
|
| 110 | +Return an array with size (n_scenarios, N_STAGES) |
| 111 | +
|
| 112 | +""" |
| 113 | +function build_scenarios(n_scenarios::Int64, probabilities::Array{Float64}) |
| 114 | + scenarios = zeros(n_scenarios, N_STAGES) |
| 115 | + |
| 116 | + for scen in 1:n_scenarios |
| 117 | + for t in 1:N_STAGES |
| 118 | + Pcum = cumsum(probabilities[:, t]) |
| 119 | + |
| 120 | + n_random = rand() |
| 121 | + prob = findfirst(x -> x > n_random, Pcum) |
| 122 | + scenarios[scen, t] = prob |
| 123 | + end |
| 124 | + end |
| 125 | + return scenarios |
| 126 | +end |
| 127 | + |
| 128 | + |
| 129 | +"""Build n_scenarios according to a given distribution. |
| 130 | +
|
| 131 | +Return a Vector{NoiseLaw}""" |
| 132 | +function generate_probability_laws(n_scenarios) |
| 133 | + aleas = build_scenarios(n_scenarios, build_aleas()) |
| 134 | + |
| 135 | + laws = Vector{NoiseLaw}(N_STAGES) |
| 136 | + |
| 137 | + # uniform probabilities: |
| 138 | + proba = 1/n_scenarios*ones(n_scenarios) |
| 139 | + |
| 140 | + for t=1:N_STAGES |
| 141 | + laws[t] = NoiseLaw(aleas[:, t], proba) |
| 142 | + end |
| 143 | + |
| 144 | + return laws |
| 145 | +end |
| 146 | + |
| 147 | + |
| 148 | +"""Instantiate the problem. |
| 149 | +
|
| 150 | +Return a tuple (SPModel, SDDPparameters) |
| 151 | +
|
| 152 | +""" |
| 153 | +function init_problem() |
| 154 | + |
| 155 | + N_SCENARIOS = 10 |
| 156 | + |
| 157 | + x0 = [50, 50] |
| 158 | + aleas = generate_probability_laws(N_SCENARIOS) |
| 159 | + |
| 160 | + # Constants: |
| 161 | + VOLUME_MAX = 100 |
| 162 | + VOLUME_MIN = 0 |
| 163 | + CONTROL_MAX = round(Int, .4/7. * VOLUME_MAX) + 1 |
| 164 | + CONTROL_MIN = 0 |
| 165 | + |
| 166 | + |
| 167 | + |
| 168 | + x_bounds = [(VOLUME_MIN, VOLUME_MAX), (VOLUME_MIN, VOLUME_MAX)] |
| 169 | + u_bounds = [(CONTROL_MIN, CONTROL_MAX), (CONTROL_MIN, CONTROL_MAX), (0, Inf), (0, Inf)] |
| 170 | + |
| 171 | + model = LinearDynamicLinearCostSPmodel(N_STAGES, |
| 172 | + u_bounds, |
| 173 | + x0, |
| 174 | + cost_t, |
| 175 | + dynamic, |
| 176 | + aleas) |
| 177 | + |
| 178 | + set_state_bounds(model, x_bounds) |
| 179 | + |
| 180 | + |
| 181 | + EPSILON = .05 |
| 182 | + MAX_ITER = 20 |
| 183 | + solver = SOLVER |
| 184 | + params = SDDPparameters(solver, N_SCENARIOS, EPSILON, MAX_ITER) |
| 185 | + |
| 186 | + return model, params |
| 187 | +end |
| 188 | + |
| 189 | + |
| 190 | +"""Benchmark SDDP.""" |
| 191 | +function benchmark_sddp() |
| 192 | + model, params = init_problem() |
| 193 | + |
| 194 | + # Launch a first start to compile solve_SDDP |
| 195 | + params.maxItNumber = 2 |
| 196 | + V, pbs = solve_SDDP(model, params, 0) |
| 197 | + params.maxItNumber = 20 |
| 198 | + # Launch benchmark |
| 199 | + println("Launch SDDP ...") |
| 200 | + tic() |
| 201 | + V, pbs = solve_SDDP(model, params, 0) |
| 202 | + texec = toq() |
| 203 | + println("Time to solve SDDP: ", texec, "s") |
| 204 | + |
| 205 | + # Test results upon 100 assessment scenarios: |
| 206 | + n_assessments = 100 |
| 207 | + aleas = simulate_scenarios(model.noises, |
| 208 | + (model.stageNumber, |
| 209 | + n_assessments, |
| 210 | + model.dimNoises)) |
| 211 | + |
| 212 | + tic() |
| 213 | + costs_sddp, stocks = forward_simulations(model, params, V, pbs, aleas) |
| 214 | + texec = toq() |
| 215 | + println("Time to perform simulation: ", texec, "s") |
| 216 | + |
| 217 | + # Get costs with deterministic solution: |
| 218 | + println("Compute anticipative solution ...") |
| 219 | + costs_det = zeros(n_assessments) |
| 220 | + for n in 1:n_assessments |
| 221 | + costs_det[n] = solve_determinist_problem(model, aleas[:, n, :]) |
| 222 | + end |
| 223 | + |
| 224 | + println("SDDP cost: \t", mean(costs_sddp)) |
| 225 | + println("Deterministic cost: \t", mean(costs_det)) |
| 226 | + println("Gap: \t", mean(costs_det)/mean(costs_sddp)) |
| 227 | + return stocks, V |
| 228 | +end |
| 229 | + |
| 230 | + |
| 231 | +"""Benchmark SDP.""" |
| 232 | +function benchmark_sdp() |
| 233 | + |
| 234 | + N_STAGES::Int64 = 5 |
| 235 | + TF = N_STAGES |
| 236 | + # Capacity of dams: |
| 237 | + VOLUME_MAX::Float64 = 20. |
| 238 | + VOLUME_MIN::Float64 = 0. |
| 239 | + |
| 240 | + # Specify the maximum flow of turbines: |
| 241 | + CONTROL_MAX::Float64 = 5. |
| 242 | + CONTROL_MIN::Float64 = 0. |
| 243 | + |
| 244 | + # Some statistics about aleas (water inflow): |
| 245 | + W_MAX::Float64 = 5. |
| 246 | + W_MIN::Float64 = 0. |
| 247 | + DW::Float64 = 1. |
| 248 | + |
| 249 | + T0 = 1 |
| 250 | + |
| 251 | + # Define aleas' space: |
| 252 | + N_ALEAS = Int(round(Int, (W_MAX - W_MIN) / DW + 1)) |
| 253 | + ALEAS = linspace(W_MIN, W_MAX, N_ALEAS); |
| 254 | + |
| 255 | + N_CONTROLS = 2; |
| 256 | + N_STATES = 2; |
| 257 | + N_NOISES = 1; |
| 258 | + |
| 259 | + infoStruct = "HD" |
| 260 | + |
| 261 | + COST = 66*2.7*(1 + .5*(rand(TF) - .5)); |
| 262 | + |
| 263 | + # Define dynamic of the dam: |
| 264 | + function dynamic(t, x, u, w) |
| 265 | + return [x[1] + u[1] + w[1] - u[2], x[2] - u[1]] |
| 266 | + end |
| 267 | + |
| 268 | + # Define cost corresponding to each timestep: |
| 269 | + function cost_t(t, x, u, w) |
| 270 | + return COST[t] * u[1] |
| 271 | + end |
| 272 | + |
| 273 | + function constraints(t, x, u, w) |
| 274 | + return (VOLUME_MIN<=x[1]<=VOLUME_MAX)&(VOLUME_MIN<=x[2]<=VOLUME_MAX) |
| 275 | + end |
| 276 | + |
| 277 | + function finalCostFunction(x) |
| 278 | + return 0. |
| 279 | + end |
| 280 | + |
| 281 | + """Build admissible scenarios for water inflow over the time horizon.""" |
| 282 | + function build_scenarios(n_scenarios::Int64) |
| 283 | + scenarios = zeros(n_scenarios, TF) |
| 284 | + |
| 285 | + for scen in 1:n_scenarios |
| 286 | + scenarios[scen, :] = (W_MAX-W_MIN)*rand(TF)+W_MIN |
| 287 | + end |
| 288 | + return scenarios |
| 289 | + end |
| 290 | + |
| 291 | + """Build probability distribution at each timestep based on N scenarios. |
| 292 | + Return a Vector{NoiseLaw}""" |
| 293 | + function generate_probability_laws(N_STAGES, N_SCENARIOS) |
| 294 | + aleas = zeros(N_SCENARIOS, TF, 1) |
| 295 | + aleas[:, :, 1] = build_scenarios(N_SCENARIOS) |
| 296 | + |
| 297 | + laws = Vector{NoiseLaw}(N_STAGES) |
| 298 | + |
| 299 | + # uniform probabilities: |
| 300 | + proba = 1/N_SCENARIOS*ones(N_SCENARIOS) |
| 301 | + |
| 302 | + for t=1:N_STAGES |
| 303 | + aleas_t = reshape(aleas[:, t, :], N_SCENARIOS, 1)' |
| 304 | + laws[t] = NoiseLaw(aleas_t, proba) |
| 305 | + end |
| 306 | + |
| 307 | + return laws |
| 308 | + end |
| 309 | + |
| 310 | + N_SCENARIO = 5 |
| 311 | + aleas = generate_probability_laws(TF, N_SCENARIO) |
| 312 | + |
| 313 | + x_bounds = [(VOLUME_MIN, VOLUME_MAX), (VOLUME_MIN, VOLUME_MAX)]; |
| 314 | + u_bounds = [(CONTROL_MIN, CONTROL_MAX), (VOLUME_MIN, 10)]; |
| 315 | + |
| 316 | + x0 = [20., 22.] |
| 317 | + |
| 318 | + modelSDP = StochDynProgModel(N_STAGES-1, N_CONTROLS, |
| 319 | + N_STATES, N_NOISES, |
| 320 | + x_bounds, u_bounds, |
| 321 | + x0, cost_t, |
| 322 | + finalCostFunction, dynamic, |
| 323 | + constraints, aleas); |
| 324 | + |
| 325 | + stateSteps = [1.,1.]; |
| 326 | + controlSteps = [1.,1.]; |
| 327 | + monteCarloSize = 10.; |
| 328 | + |
| 329 | + paramsSDP = StochDynamicProgramming.SDPparameters(modelSDP, stateSteps, |
| 330 | + controlSteps, |
| 331 | + monteCarloSize, |
| 332 | + infoStruct); |
| 333 | + |
| 334 | + print("Problem size (T*X*U*W): ") |
| 335 | + println(paramsSDP.totalStateSpaceSize*paramsSDP.totalControlSpaceSize) |
| 336 | + |
| 337 | + n_benchmark = 10 |
| 338 | + timing = zeros(n_benchmark) |
| 339 | + for n in 1:n_benchmark |
| 340 | + tic() |
| 341 | + V_sdp = sdp_optimize(modelSDP, paramsSDP,false); |
| 342 | + timing[n] = toq() |
| 343 | + end |
| 344 | + @show timing |
| 345 | + println("SDP execution time: ", mean(timing[2:end]), " s +/-", 3.std(timing[2:end])) |
| 346 | + |
| 347 | +end |
| 348 | + |
| 349 | +# SDDP benchmark: |
| 350 | +if ARGS[1] == "SDDP" |
| 351 | + benchmark_sddp() |
| 352 | +elseif ARGS[1] == "DP" |
| 353 | + benchmark_sdp() |
| 354 | +end |
0 commit comments