Skip to content

Commit 29bc922

Browse files
authored
Add variable bounds to incoming states in LagrangianDuality (#819)
1 parent 1730279 commit 29bc922

File tree

6 files changed

+182
-29
lines changed

6 files changed

+182
-29
lines changed

docs/src/guides/add_multidimensional_noise.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ julia> model = SDDP.LinearPolicyGraph(
2626
SDDP.parameterize(subproblem, Ω, P) do ω
2727
JuMP.fix(x.out, ω.value)
2828
@stageobjective(subproblem, ω.coefficient * x.out)
29-
println("ω is: ", ω)
3029
end
3130
end;
3231
33-
julia> SDDP.simulate(model, 1);
32+
julia> for s in SDDP.simulate(model, 1)[1]
33+
println("ω is: ", s[:noise_term])
34+
end
3435
ω is: (value = 1, coefficient = 4)
3536
ω is: (value = 1, coefficient = 3)
3637
ω is: (value = 2, coefficient = 4)
@@ -46,7 +47,7 @@ For sampling multidimensional random variates, it can be useful to use the
4647
There are several ways to go about this. If the sample space is finite, and
4748
small enough that it makes sense to enumerate each element, we can use
4849
`Base.product` and `Distributions.support` to generate the entire sample space
49-
`Ω` from each of the marginal distributions.
50+
`Ω` from each of the marginal distributions.
5051

5152
We can then evaluate the density function of the product distribution on each
5253
element of this space to get the vector of corresponding probabilities, `P`.
@@ -75,11 +76,12 @@ julia> model = SDDP.LinearPolicyGraph(
7576
SDDP.parameterize(subproblem, Ω, P) do ω
7677
JuMP.fix(x.out, ω[1])
7778
@stageobjective(subproblem, ω[2] * x.out + ω[3])
78-
println("ω is: ", ω)
7979
end
8080
end;
8181
82-
julia> SDDP.simulate(model, 1);
82+
julia> for s in SDDP.simulate(model, 1)[1]
83+
println("ω is: ", s[:noise_term])
84+
end
8385
ω is: [10, 0, 3]
8486
ω is: [0, 1, 6]
8587
ω is: [6, 0, 5]
@@ -117,11 +119,12 @@ julia> model = SDDP.LinearPolicyGraph(
117119
SDDP.parameterize(subproblem, Ω, P) do ω
118120
JuMP.fix(x.out, ω[1])
119121
@stageobjective(subproblem, ω[2] * x.out + ω[3])
120-
println("ω is: ", ω)
121122
end
122123
end;
123124
124-
julia> SDDP.simulate(model, 1);
125+
julia> for s in SDDP.simulate(model, 1)[1]
126+
println("ω is: ", s[:noise_term])
127+
end
125128
ω is: [54, 38, 19]
126129
ω is: [43, 3, 13]
127130
ω is: [43, 4, 17]

docs/src/guides/add_noise_in_the_constraint_matrix.md

+2-7
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,15 @@ julia> model = SDDP.LinearPolicyGraph(
1515
stages=3, lower_bound = 0, optimizer = HiGHS.Optimizer
1616
) do subproblem, t
1717
@variable(subproblem, x, SDDP.State, initial_value = 0.0)
18-
@constraint(subproblem, emissions, 1x.out <= 1)
18+
@constraint(subproblem, emissions, 1 * x.out <= 1)
1919
SDDP.parameterize(subproblem, [0.2, 0.5, 1.0]) do ω
20+
## rewrite 1 * x.out <= 1 to ω * x.out <= 1
2021
JuMP.set_normalized_coefficient(emissions, x.out, ω)
21-
println(emissions)
2222
end
2323
@stageobjective(subproblem, -x.out)
2424
end
2525
A policy graph with 3 nodes.
2626
Node indices: 1, 2, 3
27-
28-
julia> SDDP.simulate(model, 1);
29-
emissions : x_out <= 1
30-
emissions : 0.2 x_out <= 1
31-
emissions : 0.5 x_out <= 1
3227
```
3328

3429
!!! note

src/plugins/duality_handlers.jl

+13
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,16 @@ function get_dual_solution(node::Node, lagrange::LagrangianDuality)
179179
x_in_value[i] = JuMP.fix_value(state.in)
180180
h_expr[i] = @expression(node.subproblem, state.in - x_in_value[i])
181181
JuMP.unfix(state.in)
182+
l, u, is_integer = node.incoming_state_bounds[key]
183+
if l > -Inf
184+
JuMP.set_lower_bound(state.in, l)
185+
end
186+
if u < Inf
187+
JuMP.set_upper_bound(state.in, u)
188+
end
189+
if is_integer
190+
JuMP.set_integer(state.in)
191+
end
182192
λ_star[i] = conic_dual[key]
183193
end
184194
# Check that the conic dual is feasible for the subproblem. Sometimes it
@@ -194,6 +204,9 @@ function get_dual_solution(node::Node, lagrange::LagrangianDuality)
194204
return L_k === nothing ? nothing : (s * L_k, s * h_k)
195205
end
196206
for (i, (_, state)) in enumerate(node.states)
207+
if JuMP.is_integer(state.in)
208+
JuMP.unset_integer(state.in)
209+
end
197210
JuMP.fix(state.in, x_in_value[i]; force = true)
198211
end
199212
λ_solution = Dict{Symbol,Float64}(

src/user_interface.jl

+77
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,8 @@ mutable struct Node{T}
655655
ext::Dict{Symbol,Any}
656656
# Lock for threading
657657
lock::ReentrantLock
658+
# (lower, upper, is_integer)
659+
incoming_state_bounds::Dict{Symbol,Tuple{Float64,Float64,Bool}}
658660
end
659661

660662
function Base.show(io::IO, node::Node)
@@ -987,6 +989,7 @@ function PolicyGraph(
987989
# The extension dictionary.
988990
Dict{Symbol,Any}(),
989991
ReentrantLock(),
992+
Dict{Symbol,Tuple{Float64,Float64,Bool}}(),
990993
)
991994
subproblem.ext[:sddp_policy_graph] = policy_graph
992995
policy_graph.nodes[node_index] = subproblem.ext[:sddp_node] = node
@@ -1032,9 +1035,83 @@ function PolicyGraph(
10321035
if length(graph.belief_partition) > 0
10331036
initialize_belief_states(policy_graph, graph)
10341037
end
1038+
domain = _get_incoming_domain(policy_graph)
1039+
for (node_name, node) in policy_graph.nodes
1040+
for (k, v) in domain[node_name]
1041+
node.incoming_state_bounds[k] = something(v, (-Inf, Inf, false))
1042+
end
1043+
end
10351044
return policy_graph
10361045
end
10371046

1047+
function _get_incoming_domain(model::PolicyGraph{T}) where {T}
1048+
function _bounds(x)
1049+
l, u = -Inf, Inf
1050+
if has_lower_bound(x)
1051+
l = lower_bound(x)
1052+
end
1053+
if has_upper_bound(x)
1054+
u = upper_bound(x)
1055+
end
1056+
if is_fixed(x)
1057+
l = u = fix_value(x)
1058+
end
1059+
is_int = is_integer(x)
1060+
if is_binary(x)
1061+
l, u = max(something(l, 0.0), 0.0), min(something(u, 1.0), 1.0)
1062+
is_int = true
1063+
end
1064+
return l, u, is_int
1065+
end
1066+
outgoing_bounds = Dict{Tuple{T,Symbol},Any}(
1067+
(k, state_name) => nothing for (k, node) in model.nodes for
1068+
(state_name, _) in node.states
1069+
)
1070+
for (k, node) in model.nodes
1071+
for noise in node.noise_terms
1072+
parameterize(node, noise.term)
1073+
for (state_name, state) in node.states
1074+
domain = outgoing_bounds[(k, state_name)]
1075+
l_new, u_new, is_int_new = _bounds(state.out)
1076+
outgoing_bounds[(k, state_name)] = if domain === nothing
1077+
(l_new, u_new, is_int_new)
1078+
else
1079+
l, u, is_int = domain::Tuple{Float64,Float64,Bool}
1080+
(min(l, l_new), max(u, u_new), is_int & is_int_new)
1081+
end
1082+
end
1083+
end
1084+
end
1085+
incoming_bounds = Dict{T,Dict{Symbol,Any}}()
1086+
for (k, node) in model.nodes
1087+
incoming_bounds[k] = Dict{Symbol,Any}(
1088+
state_name => nothing for (state_name, _) in node.states
1089+
)
1090+
end
1091+
for (parent_name, parent) in model.nodes
1092+
for (state_name, state) in parent.states
1093+
domain_new = outgoing_bounds[(parent_name, state_name)]
1094+
l_new, u_new, is_int_new = domain_new::Tuple{Float64,Float64,Bool}
1095+
for child in parent.children
1096+
domain = incoming_bounds[child.term][state_name]
1097+
incoming_bounds[child.term][state_name] = if domain === nothing
1098+
(l_new, u_new, is_int_new)
1099+
else
1100+
l, u, is_int = domain::Tuple{Float64,Float64,Bool}
1101+
(min(l, l_new), max(u, u_new), is_int & is_int_new)
1102+
end
1103+
end
1104+
end
1105+
end
1106+
# The incoming state from the root node can be anything
1107+
for (state_name, value) in model.initial_root_state
1108+
for child in model.root_children
1109+
incoming_bounds[child.term][state_name] = (-Inf, Inf, false)
1110+
end
1111+
end
1112+
return incoming_bounds
1113+
end
1114+
10381115
# Internal function: set up ::BeliefState for each node.
10391116
function initialize_belief_states(
10401117
policy_graph::PolicyGraph{T},

test/plugins/sampling_schemes.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,8 @@ function test_SimulatorSamplingScheme_with_noise()
380380
) do sp, node
381381
t, price = node
382382
@variable(sp, 0 <= x <= 1, SDDP.State, initial_value = 0)
383-
SDDP.parameterize(sp, [(price, i) for i in 1:2]) do ω
384-
return SDDP.@stageobjective(sp, price * x.out + i)
383+
SDDP.parameterize(sp, [(price, i) for i in 1:2]) do (p, i)
384+
return SDDP.@stageobjective(sp, p * x.out + i)
385385
end
386386
end
387387
sampler = SDDP.SimulatorSamplingScheme(simulator)

test/user_interface.jl

+78-13
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,8 @@ function test_parameterize()
305305
end
306306
node = model[2]
307307
@test length(node.noise_terms) == 3
308-
@test JuMP.upper_bound(node.subproblem[:x]) == 1
308+
# SDDP is allowed to call `parameterize` during construction
309+
# @test JuMP.upper_bound(node.subproblem[:x]) == 1
309310
node.parameterize(node.noise_terms[2].term)
310311
@test JuMP.upper_bound(node.subproblem[:x]) == 2
311312
node.parameterize(3)
@@ -486,20 +487,19 @@ function test_numerical_stability_report()
486487
end
487488

488489
function test_objective_state()
489-
model = SDDP.LinearPolicyGraph(;
490-
stages = 2,
491-
lower_bound = 0,
492-
optimizer = HiGHS.Optimizer,
493-
) do subproblem, t
494-
@variable(subproblem, x, SDDP.State, initial_value = 0)
495-
SDDP.parameterize(subproblem, [1, 2]) do ω
496-
price = SDDP.objective_state(subproblem)
497-
@stageobjective(subproblem, price * x.out)
498-
end
499-
end
500490
@test_throws(
501491
ErrorException("No objective state defined."),
502-
SDDP.simulate(model, 1; parallel_scheme = SDDP.Serial()),
492+
SDDP.LinearPolicyGraph(;
493+
stages = 2,
494+
lower_bound = 0,
495+
optimizer = HiGHS.Optimizer,
496+
) do subproblem, t
497+
@variable(subproblem, x, SDDP.State, initial_value = 0)
498+
SDDP.parameterize(subproblem, [1, 2]) do ω
499+
price = SDDP.objective_state(subproblem)
500+
@stageobjective(subproblem, price * x.out)
501+
end
502+
end,
503503
)
504504
@test_throws(
505505
ErrorException("add_objective_state can only be called once."),
@@ -911,6 +911,71 @@ function test_print_problem_statistics()
911911
return
912912
end
913913

914+
function test_incoming_state_bounds()
915+
graph = SDDP.Graph(0)
916+
SDDP.add_node.((graph,), 1:4)
917+
SDDP.add_edge(graph, 0 => 4, 1.0)
918+
SDDP.add_edge(graph, 4 => 1, 0.5)
919+
SDDP.add_edge(graph, 4 => 2, 0.5)
920+
SDDP.add_edge(graph, 1 => 2, 1.0)
921+
SDDP.add_edge(graph, 2 => 3, 0.5)
922+
SDDP.add_edge(graph, 3 => 1, 0.5)
923+
SDDP.add_edge(graph, 3 => 4, 0.5)
924+
model = SDDP.PolicyGraph(graph; lower_bound = 0.0) do sp, node
925+
@variable(sp, x, Int, SDDP.State, initial_value = 0)
926+
@variable(sp, y, SDDP.State, initial_value = -1)
927+
@variable(sp, z, SDDP.State, initial_value = 1)
928+
if node == 1
929+
set_binary(z.out)
930+
SDDP.parameterize(sp, 1:2) do w
931+
JuMP.set_lower_bound(x.out, -w)
932+
JuMP.set_upper_bound(y.out, w)
933+
return
934+
end
935+
elseif node == 2
936+
SDDP.parameterize(sp, 1:2) do w
937+
if w == 1
938+
set_upper_bound(y.out, 1.0)
939+
elseif w == 2 && has_upper_bound(y.out)
940+
delete_upper_bound(y.out)
941+
end
942+
return
943+
end
944+
elseif node == 3
945+
set_lower_bound(x.out, 1)
946+
set_upper_bound(x.out, 2)
947+
fix(y.out, 3)
948+
elseif node == 4
949+
fix(x.out, 0)
950+
fix(y.out, -1)
951+
fix(z.out, 1)
952+
end
953+
@stageobjective(sp, 0)
954+
return
955+
end
956+
@test model[1].incoming_state_bounds == Dict(
957+
:x => (0.0, 2.0, true),
958+
:y => (-1.0, 3.0, false),
959+
:z => (-Inf, Inf, false),
960+
)
961+
@test model[2].incoming_state_bounds == Dict(
962+
:x => (-2.0, Inf, true),
963+
:y => (-Inf, 2.0, false),
964+
:z => (0.0, 1.0, false),
965+
)
966+
@test model[3].incoming_state_bounds == Dict(
967+
:x => (-Inf, Inf, true),
968+
:y => (-Inf, Inf, false),
969+
:z => (-Inf, Inf, false),
970+
)
971+
@test model[4].incoming_state_bounds == Dict(
972+
:x => (-Inf, Inf, false),
973+
:y => (-Inf, Inf, false),
974+
:z => (-Inf, Inf, false),
975+
)
976+
return
977+
end
978+
914979
end # module
915980

916981
TestUserInterface.runtests()

0 commit comments

Comments
 (0)