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

Compute dual variables with continous state variables and mixed control variables #708

Closed
Thiago-NovaesB opened this issue Nov 8, 2023 · 6 comments

Comments

@Thiago-NovaesB
Copy link

Thiago-NovaesB commented Nov 8, 2023

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.

@odow
Copy link
Owner

odow commented Nov 8, 2023

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"

@Thiago-NovaesB
Copy link
Author

Perfect! I'll test this, thanks!

@odow
Copy link
Owner

odow commented Nov 17, 2023

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.

@Thiago-NovaesB
Copy link
Author

Thiago-NovaesB commented Nov 17, 2023

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.

@odow
Copy link
Owner

odow commented Nov 17, 2023

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.

@odow
Copy link
Owner

odow commented Nov 19, 2023

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

@odow odow closed this as completed Nov 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants