odow / SDDP.jl

Stochastic Dual Dynamic Programming in Julia

Home Page:https://sddp.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Compute dual variables with continous state variables and mixed control variables

Thiago-NovaesB opened this issue · comments

https://sddp.dev/stable/guides/add_integrality/

I'm interested in a problem that has continuous state variables, but continuous and binary control variables.
Reading the section on integrality of the documentation, it seems to me that we can have the dual the relaxed MIP (then LP) or Lagrangian Dual.

My problem actually has only 1 feasible point for the binary part, so it would be perfect if the dual could be calculated as follows:

  1. Solve the MIP
  2. Fix the binaries variables.
  3. Solve the LP
  4. Get dual.

It is possible? I wouldn't want to relax integrality.

So the way to handle this would be to write a new duality handler. I think it would be similar to:

# ========================= Continuous relaxation ============================ #
"""
ContinuousConicDuality()
Compute dual variables in the backward pass using conic duality, relaxing any
binary or integer restrictions as necessary.
## Theory
Given the problem
```
min Cᵢ(x̄, u, w) + θᵢ
st (x̄, x′, u) in Xᵢ(w) ∩ S
x̄ - x == 0 [λ]
```
where `S ⊆ ℝ×ℤ`, we relax integrality and using conic duality to solve for `λ`
in the problem:
```
min Cᵢ(x̄, u, w) + θᵢ
st (x̄, x′, u) in Xᵢ(w)
x̄ - x == 0 [λ]
```
"""
struct ContinuousConicDuality <: AbstractDualityHandler end
function get_dual_solution(node::Node, ::ContinuousConicDuality)
if JuMP.dual_status(node.subproblem) != JuMP.MOI.FEASIBLE_POINT
# Attempt to recover by resetting the optimizer and re-solving.
if JuMP.mode(node.subproblem) != JuMP.DIRECT
MOI.Utilities.reset_optimizer(node.subproblem)
optimize!(node.subproblem)
end
end
if JuMP.dual_status(node.subproblem) != JuMP.MOI.FEASIBLE_POINT
write_subproblem_to_file(
node,
"subproblem.mof.json",
throw_error = true,
)
end
# Note: due to JuMP's dual convention, we need to flip the sign for
# maximization problems.
dual_sign = JuMP.objective_sense(node.subproblem) == MOI.MIN_SENSE ? 1 : -1
λ = Dict{Symbol,Float64}(
name => dual_sign * JuMP.dual(JuMP.FixRef(state.in)) for
(name, state) in node.states
)
return objective_value(node.subproblem), λ
end
function _relax_integrality(node::Node)
if !node.has_integrality
return () -> nothing
end
return JuMP.relax_integrality(node.subproblem)
end
function prepare_backward_pass(node::Node, ::ContinuousConicDuality, ::Options)
return _relax_integrality(node)
end
duality_log_key(::ContinuousConicDuality) = " "

except that instead of relaxing the integrality, we'd need to use fix_discrete_variables:

https://jump.dev/JuMP.jl/stable/tutorials/linear/mip_duality/#Use-fix_discrete_variables

Perhaps something like (untested)

struct FixedDiscreteDuality <: SDDP.AbstractDualityHandler end

function get_dual_solution(node::SDDP.Node, ::FixedDiscreteDuality)
    undo = JuMP.fix_discrete_variables(node.subproblem)
    JuMP.optimize!(node.subproblem)
    if JuMP.dual_status(node.subproblem) != JuMP.MOI.FEASIBLE_POINT
        SDDP.write_subproblem_to_file(
            node,
            "subproblem.mof.json",
            throw_error = true,
        )
    end
    dual_sign = JuMP.objective_sense(node.subproblem) == MOI.MIN_SENSE ? 1 : -1
    λ = Dict{Symbol,Float64}(
        name => dual_sign * JuMP.dual(JuMP.FixRef(state.in)) for
        (name, state) in node.states
    )
    V = JuMP.objective_value(node.subproblem)
    undo()
    return V, λ
end

SDDP.prepare_backward_pass(::Node, ::FixedDiscreteDuality, ::Options) = nothing

SDDP.duality_log_key(::FixedDiscreteDuality) = "F"

Perfect! I'll test this, thanks!

Did you end up testing this? I'm hesitant to add this as an option to SDDP.jl. It feels like it'd work only in very specific situations, and that it'd be too easy to use it when the model doesn't suit.

I haven't had time to test this yet, I should do it this weekend. If you want, you can close the issue and I will reopen it if something goes wrong. But I'm almost certain that your example should work or a small variation of it.

Regarding adding it to SDDP.jl, I was going to suggest that, but thinking about it further I'm also unsure whether it's a good idea. For me reasons not to add:

  1. From a theoretical point of view, this does not seem to me to be (by definition) a subgradient (Warning this in the documentation?).
  2. I think that if we have interger as state variables, the code will break (add an if in this function to check this?)
  3. In a general case, it will generate cuts that may say that the cost in certain states is very high, without knowing that with other interger values this cost would be lower.

Reasons to add:

  1. Relaxing interger variables seems to me to generate suboptimal solutions, as it is optimistic in relation to the future cost. This would be an option to be pessimistic regarding the future cost, that is, to generate the optimal policy for a subset of the original possible choices.
  2. In practice, this seems to be widely used in Brazil, for marginal cost. I remember being surprised when I saw that SDDP.jl relaxed instead of fixed (but I understand that it makes more sense given the points above.).
  3. Still on the previous point, after training the model, during the simulation it is possible to fix all integers, even state variables (here we will not generate cuts, so there is no problem). And then get the dual variables.
    For hydrothermal dispatch, the integer state variables are usually whether a generator is turned on or not. There is not much practical interest in this "dual", the most important thing is the cost of water.

For me reasons not to add:

Yes, precisely.

Reasons to add:

  1. Correct. But only for the dual on the backward pass. We still do the forward pass with the integer variables. You could try StrengthenedConicDuality() as an improvement over ConicDuality() without having the solve the full Lagrangian.
  2. Yeah, we have a tutorial about this inn JuMP: https://jump.dev/JuMP.jl/stable/tutorials/linear/mip_duality/.
  3. I've thought about this, but I don't think it makes much sense. I'd prefer that people realize MIPs do not have dual solutions.

If you want, you can close the issue and I will reopen it if something goes wrong. But I'm almost certain that your example should work or a small variation of it.

I think this sums it up. It seems like we're in agreement here on the pros and cons, and that it isn't obvious which decision is the best. For the code code, I'd prefer to default no, rather than default yes.

You're welcome to develop this in an external package, and if you need any internal parts of SDDP.jl changed to help then we can do that.

Closing this as won't implement. (But happy to make any changes that you need to implement this as an external plug-in.)