Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: entropic paper #460

Merged
merged 1 commit into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions papers/entropic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
solution_*.j
stage_objective*.csv
*.pdf
*.dat
10 changes: 10 additions & 0 deletions papers/entropic/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[deps]
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
SDDP = "f4570300-c277-11e8-125c-4912f86ce65d"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd"
8 changes: 8 additions & 0 deletions papers/entropic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Multistage stochastic programs with the entropic risk measure

The code in this file runs the examples from the working paper

Dowson, O., Morton, D.P., and Pagnoncelli, B.K., Multistage stochastic programs
with the entropic risk measure.

[http://www.optimization-online.org/DB_HTML/2020/08/7984.html](http://www.optimization-online.org/DB_HTML/2020/08/7984.html)
1 change: 1 addition & 0 deletions papers/entropic/data.json

Large diffs are not rendered by default.

301 changes: 301 additions & 0 deletions papers/entropic/model.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# Copyright 2019-21, Oscar Dowson, Lingquan Ding (@lingquant).
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This example is based on one from MSPPy:
# https://github.com/lingquant/msppy/blob/dc85a2e8fa5243b3d5096d59085d9caad3ff2ede/examples/hydro_thermal/julia/test.jl
#
# The original author was Lingquan Ding (@lingquant), but it was modified by
# Oscar Dowson (@odow) to meet the latest SDDP.jl syntax.
#
# The original model and data is from:
#
# Shapiro, A., Tekaya, W., da Costa, J. P., & Soares, M. P. (2013). Risk neutral
# and risk averse stochastic dual dynamic programming method. European journal
# of operational research, 224(2), 375–391.

using SDDP

import DelimitedFiles
import Gurobi
import JSON
import Plots
import Random
import Statistics
import StatsPlots

function _build_model(data::Dict)
N_THERMAL = length.(data["thermal_obj"])
@assert N_THERMAL == length.(data["thermal_lb"])
@assert N_THERMAL == length.(data["thermal_ub"])
I = [1, 2, 3, 4]
K(i) = 1:N_THERMAL[i]
IM = 5
I_IM = union(I, IM)
env = Gurobi.Env()
function gurobi_optimizer()
model = Gurobi.Optimizer(env)
MOI.set(model, MOI.Silent(), true)
return model
end
model = SDDP.LinearPolicyGraph(
stages = 60,
lower_bound = 0.0,
optimizer = gurobi_optimizer,
) do sp, t
set_silent(sp)
month = t % 12 == 0 ? 12 : t % 12
@variable(
sp,
0 <= v[i in I] <= data["storedEnergy_ub"][i] / 1_000,
SDDP.State,
initial_value = data["storedEnergy_initial"][i] / 1_000,
)
@variable(sp, s[i in I] >= 0.0)
@variable(sp, 0.0 <= q[i in I] <= data["hydro_ub"][i] / 1_000)
@variable(
sp,
g[i in I, k in K(i)],
lower_bound = data["thermal_lb"][i][k] / 1_000,
upper_bound = data["thermal_ub"][i][k] / 1_000,
)
@variable(
sp,
0 <= ex[i in I_IM, j in I_IM] <= data["exchange_ub"][i][j] / 1_000,
)
@variable(
sp,
df[i in I, j in I] >= 0,
upper_bound =
data["demand"][month][i] * data["deficit_ub"][j] / 1_000,
)
@stageobjective(
sp,
sum(
data["deficit_obj"][i] * sum(df[i, :]) +
sum(data["thermal_obj"][i][k] * g[i, k] for k in K(i)) for
i in I
)
)
@constraint(
sp,
[i in I],
q[i] + sum(g[i, k] for k in K(i)) + sum(df[i, :]) + sum(ex[:, i]) - sum(ex[i, :]) ==
data["demand"][month][i] / 1_000
)
@constraint(
sp,
balance[i in I],
v[i].out ==
v[i].in + data["inflow_initial"][i] / 1_000 - s[i] - q[i]
)
@constraint(sp, sum(ex[:, IM]) == sum(ex[IM, :]))
if t == 1
# Deterministic first stage with `inflow_initial`.
else
r = (t - 1) % 12 == 0 ? 12 : (t - 1) % 12
Ω = data["scenarios"]
# To simplify things. Don't use all the scenarios!
num_scenarios = min(40, length(Ω[1][r]))
SDDP.parameterize(sp, 1:num_scenarios) do ω
for i in 1:4
set_normalized_rhs(balance[i], Ω[i][r][ω] / 1_000)
end
end
end
end
return model
end

function _train_model(model::SDDP.PolicyGraph, gamma::Float64)
risk_measure = isfinite(gamma) ? SDDP.Entropic(gamma) : SDDP.WorstCase()
# Set the same random seed for all runs!
Random.seed!(1234)
psr_sampling_scheme = SDDP.PSRSamplingScheme(1_000)
try
SDDP.train(
model;
iteration_limit = 10_000,
risk_measure = risk_measure,
sampling_scheme = psr_sampling_scheme,
log_file = "$(gamma).log",
)
catch ex
@info "Ignorring error"
println("$(ex)")
end
# Set the same random seed for all runs!
Random.seed!(12345)
simulations = SDDP.simulate(
model,
1_000,
[:v, :df, :g];
sampling_scheme = psr_sampling_scheme,
)
stage_objectives = [
round(simulations[i][t][:stage_objective]; digits = 1) for
i in 1:length(simulations) for t in 1:length(simulations[i])
]
stage_objectives =
reshape(stage_objectives, length(simulations[1]), length(simulations))
open("stage_objectives_$(gamma).csv", "w") do io
return DelimitedFiles.writedlm(
io,
collect(transpose(stage_objectives)),
',',
)
end
return
end

function _plot_cumulative_density(
filenames = [
"stage_objectives_0.000000.csv",
"stage_objectives_0.000010.csv",
"stage_objectives_5.0e-5.csv",
"stage_objectives_0.000100.csv",
],
)
Plots.plot()
line_styles = [:solid, :dash, :dot, :dashdot]
for (i, f) in enumerate(filenames)
X = sum(DelimitedFiles.readdlm(f, ','); dims = 2)[:]
x = parse(Float64, match(r"stage\_objectives\_(.+)\.csv", f)[1])
x = if x ≈ 0.0
"0"
elseif x ≈ 5e-5
"5 \\times 10^{-5}"
else
"1 \\times 10^{$(Int(log10(x)))}"
end
StatsPlots.cdensity!(
X,
label = "\$\\gamma = $(x)\$",
style = line_styles[i],
color = :black,
)
end
p = Plots.plot!(
xlabel = "Cost [\$]",
ylabel = "Cumulative density",
legend = :bottomright,
size = (400, 300),
)
Plots.savefig("cumulative_density.pdf")
return p
end

function _plot_objectives(filename::String)
quantiles = [0.0, 0.01, 0.05, 0.5, 0.95, 0.99, 1.0]
matrix = DelimitedFiles.readdlm(filename, ',')
A = mapreduce(
i -> Statistics.quantile(matrix[:, i], quantiles)',
vcat,
1:size(matrix, 2),
)
x = parse(Float64, match(r"stage\_objectives\_(.+)\.csv", filename)[1])
x = if x ≈ 0.0
"0"
elseif x ≈ 5e-5
"5 \\times 10^{-5}"
else
"10^{$(Int(log10(x)))}"
end
Plots.plot(
A[:, 4],
ribbon = (A[:, 4] .- A[:, 1], A[:, 7] .- A[:, 4]),
color = :black,
fillalpha = 0.2,
legend = false,
title = "\$\\gamma = $(x)\$",
size = (300, 400),
)
Plots.plot!(
A[:, 4],
ribbon = (A[:, 4] .- A[:, 2], A[:, 6] .- A[:, 4]),
color = :black,
fillalpha = 0.2,
)
Plots.plot!(
A[:, 4],
ribbon = (A[:, 4] .- A[:, 3], A[:, 5] .- A[:, 4]),
color = :black,
fillalpha = 0.2,
)
return Plots.plot!(
A[:, 4],
color = :black,
xlabel = "Stages",
ylabel = "Stage objective (\$)",
ylim = (0, 4e4),
)
end

function _plot_objectives(
filenames::Vector{String} = [
"stage_objectives_0.000000.csv",
"stage_objectives_0.000010.csv",
"stage_objectives_5.0e-5.csv",
"stage_objectives_0.000100.csv",
],
)
p = Plots.plot(
_plot_objectives.(filenames)...,
size = (1200, 800),
margin = 5Plots.mm,
)
Plots.savefig("objectives.pdf")
return p
end

function _print_help()
return println(
"""
usage: julia [-p N] [--project=.] model.jl [--gamma=<value>] [--help] [--plot]

Solve the hydro-thermal scheduling problem with the Entropic risk measure
parameterized with γ = <value>.

Use `-p N` to run the SDDP solver in parallel mode over `N` processors.

Use `--project=.` to reproduce the example in the paper using the provided
`Manifest.toml` and `Project.toml` files.

Examples:

julia model.jl --gamma=0.5
julia -p 4 --project=. model.jl --gamma=0.5
julia -p 4 --project=. model.jl --gamma=5.0
julia --project=. model.jl --plot
""",
)
end

function main(args)
if length(args) == 0
_print_help()
elseif findfirst(isequal("--help"), args) !== nothing
_print_help()
elseif findfirst(isequal("-h"), args) !== nothing
_print_help()
else
i = findfirst(arg -> startswith(arg, "--gamma="), args)
if i !== nothing
data = JSON.parsefile(joinpath(@__DIR__, "data.json"))
model = _build_model(data)
gamma = parse(Float64, replace(args[i], "--gamma=" => ""))::Float64
_train_model(model, gamma)
end
if findfirst(isequal("--plot"), args) !== nothing
_plot_cumulative_density()
_plot_objectives()
end
end
end

# ============================================================================ #
# Script entry point! #
# ============================================================================ #

main(ARGS)
Loading