Optimize the size of a reservoir over time under uncertainty.
Suppose we have the following inputs:
Static parameters:
Choice variables:
State space variables:
Following reservoir dynamics: $$v_{t+1} = (1 - e)^T v_t + q_t - w_t$$ $$c_{t+1} = c_t + k_t$$ $v_0 = c_0 = 0$
Constrained such that,
And the objective is to minimize costs: $$\sum_t (A x_t + k_t) e^{-\delta t}$$
We have N Monte Carlo-like scenarios of the inputs, $q_{it}$ and $d_{it}$. In the example below, only precipitation will vary by scenario. Each scenario has a probability, $p_i$, s.t. $\sum p_i = 1$.
We estimate separate timeseries of the immediate choice variables, $w_{it}$ and $x_{it}$. These do not require long-term planning, and so do not necessitate a larger state-space. However, there is just one timeseries for investment choices, $k_t$.
The objective function now takes the expected value of these costs: $$\sum_t \left(A \left(\sum_i p_i x_{it}\right) + k_t\right) e^{-\delta t}$$
I propose that AWASH not do proper optimization under uncertainty. Under proper optimization, the state-space transition in each period is uncertain, which both means that optimization is taken under the knowledge that more information is provided in each period, and that the state-space needs to be enumerated.
Instead, I propose that an entire timeseries of long-term investments will be made in period $t_0$, not knowing which scenario will be entered. This investment will then be taken, the uncertainty resolved, and a new timeseries of investments will be optimized for in period $t_1$. This continues iteratively. Furthermore, it should be iteratively estimated for each of the N scenarios.
Under this plan, the number of variables only needs to multiply by the number of scenarios, and only for those variables that do not reflect long-term investments. The main loss is that the rational optimizer will design strategies that wait for uncertainty to be resolved, since it will never be resolved (in the future periods considered by the optimization, the optimizer still will not "know" which scenario it is in, as far as investment decisions are concerned). However, this is preferable to having scenario-specific investment decision variables, where the optimizer will always wait one more period to know which scenario it is in.
There is uncertainty between two scenarios. Under scenario 1, there is always enough water (a demand of 0.5 under a supply of 1.0), while the other scenario goes into periods of drought. We optimize a single timeseries of investment, in time 0.
In [1]:
using JuMP
using Clp
using Gadfly
using DataFrames
In [2]:
time = 0:49
precip1 = repeat([1], inner=length(time))
precip2 = (cos.(2*pi*time / 25) + 1) / 3 + 1/3
demand = repeat([.5], inner=length(time));
Here is what the demand and supply look like.
In [3]:
plot(x=repmat(time, 3), y=[precip1; precip2; demand], color=repeat(["Precip 1", "Precip 2", "Demand"], inner=50), Geom.line)
Out[3]:
We set up the model...
In [4]:
m = Model(solver=ClpSolver())
@variable(m, kk[time] >= 0) # Investment
@variable(m, xx1[time] >= 0) # Supersource under scenario 1
@variable(m, ww1[time] >= 0) # Withdrawals under scenario 1
@variable(m, xx2[time] >= 0) # Supersource under scenario 2
@variable(m, ww2[time] >= 0) # Withdrawals under scenario 2
Out[4]:
In [5]:
# Always satisfy demand
@constraint(m, ww1[time] + xx1[time] .>= demand[time + 1])
@constraint(m, ww2[time] + xx2[time] .>= demand[time + 1])
Out[5]:
In [6]:
for tt1 in time
# Do not overfill reservoirs (v_t < V_t)
@constraint(m, sum(precip1[tt+1] - ww1[tt] for tt in 0:tt1) <= sum(kk[tt] for tt in 0:tt1))
@constraint(m, sum(precip2[tt+1] - ww2[tt] for tt in 0:tt1) <= sum(kk[tt] for tt in 0:tt1))
# Do not withdraw more than available (w_t < v_t-1 + p_t)
if (tt1 == 0)
@constraint(m, ww1[tt1] <= precip1[tt1+1])
@constraint(m, ww2[tt1] <= precip2[tt1+1])
else
@constraint(m, ww1[tt1] <= sum(precip1[tt+1] - ww1[tt] for tt in 0:(tt1-1)) + precip1[tt1+1])
@constraint(m, ww2[tt1] <= sum(precip2[tt+1] - ww2[tt] for tt in 0:(tt1-1)) + precip2[tt1+1])
end
end
In [7]:
@objective(m, Min, sum(((100xx1[tt] + 100xx2[tt])/2 + kk[tt]) * exp(-.05tt) for tt in time))
Out[7]:
In [8]:
status = solve(m)
println(status)
println("Objective value: ", getobjectivevalue(m))
The investment quickly builds a reservoir to handle the risk of drought. Even under the high-precip scenario, the reservoir is filled up, but that excess is never used.
In [9]:
plot(x=repmat(time, 3), y=[getvalue(kk)[time]; getvalue(ww1)[time]; getvalue(ww2)[time]], color=repeat(["Investment", "Withdrawals 1", "Withdrawals 2"], inner=50), Geom.line)
Out[9]:
We do the entire optimization during each period, taking the size of the reservoir as given. In each period, the level of the reservoir and the actual precipitation come from scenario 2 (the droughty scenario).
In [10]:
kksaved = kk
Out[10]:
In [11]:
volume = Float64[0]
invested = Float64[0]
for tt0 in time
println(tt0)
subtime = time[time .>= tt0]
subm = Model(solver=ClpSolver())
@variable(subm, kk[subtime] >= 0) # Investment
@variable(subm, xx1[subtime] >= 0) # Supersource under scenario 1
@variable(subm, ww1[subtime] >= 0) # Withdrawals under scenario 1
@variable(subm, xx2[subtime] >= 0) # Supersource under scenario 2
@variable(subm, ww2[subtime] >= 0) # Withdrawals under scenario 2
for tt1 in subtime
# Always satisfy demand
@constraint(subm, ww1[tt1] + xx1[tt1] >= demand[tt1 + 1])
@constraint(subm, ww2[tt1] + xx2[tt1] >= demand[tt1 + 1])
# Do not overfill reservoirs (v_t < V_t)
@constraint(subm, volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
@constraint(subm, volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
# Do not withdraw more than available (w_t < v_t-1 + p_t)
if (tt1 == tt0)
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + precip2[tt1+1])
else
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:(tt1-1)) + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:(tt1-1)) + precip2[tt1+1])
end
end
@objective(subm, Min, sum(((100xx1[tt] + 100xx2[tt])/2 + kk[tt]) * exp(-.05tt) for tt in subtime))
status = solve(subm)
push!(invested, getvalue(kk)[tt0])
push!(volume, volume[end] + precip2[tt0+1] - getvalue(ww2)[tt0])
end
The investment does not change (the single optimization and the iterative optimization lie exactly overtop each other). The is a relief: I had worried that without the committment of a single optimization, the optimizer might keep putting off building the reservoir.
In [12]:
plot(x=repmat(time, 2), y=[getvalue(kksaved)[time]; invested[2:51]], color=repeat(["Single", "Iterative"], inner=50), Geom.line)
Out[12]:
Finally, we allow Bayesian updating of the scenario we are believed to be in, as new information comes in. Below, we do this both for the case where the true scenario is scenario 1 and for scenario 2.
Note that the probabilities are strongly weighted toward scenario 1 (99% to 1%), while they weren't in previous cases. This is because drawing from the supersource is so expensive, so even a small weight on scenario 2 results in the same results as before. I changed the probabilities to keep things interesting.
In [13]:
volume = Float64[0]
invested = Float64[0]
prob1 = .99
prob2 = .01
for tt0 in time
println(tt0)
subtime = time[time .>= tt0]
subm = Model(solver=ClpSolver())
@variable(subm, kk[subtime] >= 0) # Investment
@variable(subm, xx1[subtime] >= 0) # Supersource under scenario 1
@variable(subm, ww1[subtime] >= 0) # Withdrawals under scenario 1
@variable(subm, xx2[subtime] >= 0) # Supersource under scenario 2
@variable(subm, ww2[subtime] >= 0) # Withdrawals under scenario 2
for tt1 in subtime
# Always satisfy demand
@constraint(subm, ww1[tt1] + xx1[tt1] >= demand[tt1 + 1])
@constraint(subm, ww2[tt1] + xx2[tt1] >= demand[tt1 + 1])
# Do not overfill reservoirs (v_t < V_t)
@constraint(subm, volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
@constraint(subm, volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
# Do not withdraw more than available (w_t < v_t-1 + p_t)
if (tt1 == tt0)
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + precip2[tt1+1])
else
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:(tt1-1)) + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:(tt1-1)) + precip2[tt1+1])
end
end
@objective(subm, Min, sum(((prob1 * 100xx1[tt] + prob2 * 100xx2[tt]) + kk[tt]) * exp(-.05tt) for tt in subtime))
status = solve(subm)
push!(invested, getvalue(kk)[tt0])
push!(volume, volume[end] + precip2[tt0+1] - getvalue(ww2)[tt0])
end
volume = Float64[0]
invested = Float64[0]
prob1 = .99
prob2 = .01
for tt0 in time
println(tt0)
subtime = time[time .>= tt0]
subm = Model(solver=ClpSolver())
@variable(subm, kk[subtime] >= 0) # Investment
@variable(subm, xx1[subtime] >= 0) # Supersource under scenario 1
@variable(subm, ww1[subtime] >= 0) # Withdrawals under scenario 1
@variable(subm, xx2[subtime] >= 0) # Supersource under scenario 2
@variable(subm, ww2[subtime] >= 0) # Withdrawals under scenario 2
for tt1 in subtime
# Always satisfy demand
@constraint(subm, ww1[tt1] + xx1[tt1] >= demand[tt1 + 1])
@constraint(subm, ww2[tt1] + xx2[tt1] >= demand[tt1 + 1])
# Do not overfill reservoirs (v_t < V_t)
@constraint(subm, volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
@constraint(subm, volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
# Do not withdraw more than available (w_t < v_t-1 + p_t)
if (tt1 == tt0)
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + precip2[tt1+1])
else
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:(tt1-1)) + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:(tt1-1)) + precip2[tt1+1])
end
end
@objective(subm, Min, sum(((prob1 * 100xx1[tt] + prob2 * 100xx2[tt]) + kk[tt]) * exp(-.05tt) for tt in subtime))
status = solve(subm)
push!(invested, getvalue(kk)[tt0])
push!(volume, volume[end] + precip2[tt0+1] - getvalue(ww2)[tt0])
prob1 = .999 * prob1
prob2 = 1 - prob1
end
kksaved = invested
invested2 = invested
volume = Float64[0]
invested = Float64[0]
prob1 = .99
prob2 = .01
for tt0 in time
println(tt0)
subtime = time[time .>= tt0]
subm = Model(solver=ClpSolver())
@variable(subm, kk[subtime] >= 0) # Investment
@variable(subm, xx1[subtime] >= 0) # Supersource under scenario 1
@variable(subm, ww1[subtime] >= 0) # Withdrawals under scenario 1
@variable(subm, xx2[subtime] >= 0) # Supersource under scenario 2
@variable(subm, ww2[subtime] >= 0) # Withdrawals under scenario 2
for tt1 in subtime
# Always satisfy demand
@constraint(subm, ww1[tt1] + xx1[tt1] >= demand[tt1 + 1])
@constraint(subm, ww2[tt1] + xx2[tt1] >= demand[tt1 + 1])
# Do not overfill reservoirs (v_t < V_t)
@constraint(subm, volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
@constraint(subm, volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:tt1) <= sum(invested) + sum(kk[tt] for tt in tt0:tt1))
# Do not withdraw more than available (w_t < v_t-1 + p_t)
if (tt1 == tt0)
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + precip2[tt1+1])
else
@constraint(subm, ww1[tt1] <= volume[tt0 + 1] + sum(precip1[tt+1] - ww1[tt] for tt in tt0:(tt1-1)) + precip1[tt1+1])
@constraint(subm, ww2[tt1] <= volume[tt0 + 1] + sum(precip2[tt+1] - ww2[tt] for tt in tt0:(tt1-1)) + precip2[tt1+1])
end
end
@objective(subm, Min, sum(((prob1 * 100xx1[tt] + prob2 * 100xx2[tt]) + kk[tt]) * exp(-.05tt) for tt in subtime))
status = solve(subm)
push!(invested, getvalue(kk)[tt0])
push!(volume, volume[end] + precip1[tt0+1] - getvalue(ww1)[tt0])
prob2 = .999 * prob2
prob1 = 1 - prob1
end
invested1 = invested
Out[13]:
As evidence mounts for scenario 1 (over-supply), the reservoir is first delayed, and then only partially built. When the true scenario is 2 (droughty), the reservoir is almost completely built initially (dispite the high probability against it), and then finished during the second drought.
In [16]:
plot(x=repmat(time, 3), y=[kksaved[2:51]; invested1[2:51]; invested2[2:51]], color=repeat(["Single", "Iterative 1", "Iterative 2"], inner=50), Geom.line)
Out[16]: