Git Product home page Git Product logo

conditionaljump.jl's Introduction

ConditionalJuMP

Build Status codecov.io

This package is built on top of JuMP* and provides basic automatic generation of indicator variables, which allow (limited) statements of the form condition implies constraint in convex optimizations. It does so by automatically introducing binary indicator variables as necessary, and by using interval arithmetic to choose an appropriate big-M formulation.

* Please note that this package is not developed or maintained by the JuMP developers.

Usage

Basic Implications

using JuMP, Cbc, ConditionalJuMP

m = Model(solver=CbcSolver())
@variable(m, -1 <= x <= 1)  # having bounds on all variables is currently a requirement
@variable(m, -1 <= y <= 1)
# Require that y == 0.5 whenever x <= 0
@implies(m, (x <= 0) => (y == 0.5))
@objective(m, Min, 4x + y)
solve(m)
@assert getvalue(x)  -1
@assert getvalue(y)  0.5

Warm-starting the solver

Mixed-integer models can perform much better when given a feasible initial solution. If you've assigned initial values to your variables using JuMP's setvalue() function, then ConditionalJuMP can use those values to also add starting values for the binary indicator variables. The warmstart() function does this for you:

using JuMP, Cbc, ConditionalJuMP

m = Model(solver=CbcSolver())
@variable(m, -1 <= x <= 1)  # having bounds on all variables is currently a requirement
@variable(m, -1 <= y <= 1)
# Require that y == 0.5 whenever x <= 0
@implies(m, (x <= 0) => (y == 0.5))
@objective(m, Min, 4x + y)
# Guess a solution with x == -0.5, y == 1
setvalue(x, -0.5)
setvalue(y, 1)
warmstart!(m)
solve(m)
@assert getvalue(x)  -1
@assert getvalue(y)  0.5

Fixing the binary values

It can sometimes be useful to write a model with implication constraints, but then choose to solve that model with the implications fixed. For example, we might wish to try solving the above model both for the case that x <= 0 and for the case that x >= 0. To do that, we just call warmstart!(model, true), which will use the values previously set with setvalue() to determine exactly which sets of constraints should be applied. More concretely:

m = Model(solver=CbcSolver())
@variable(m, -1 <= x <= 1)  # having bounds on all variables is currently a requirement
@variable(m, -1 <= y <= 1)
# Require that y == 0.5 whenever x <= 0
@implies(m, (x <= 0) => (y == 0.5))
@objective(m, Min, 4x + y)

# Set up the indicator variables for the case that x == 1. Note that this does *not*
# fix x to 1 in the optimization. It simply requires that any implications which depend
# on x will be fixed to the set containing x == 1. In this case, that means that x will
# be fixed to be greater than or equal to 0. 
setvalue(x, 1)
warmstart!(m, true)
solve(m)

@assert getvalue(x)  0
@assert getvalue(y)  -1
m = Model(solver=CbcSolver())
@variable(m, -1 <= x <= 1)  # having bounds on all variables is currently a requirement
@variable(m, -1 <= y <= 1)
# Require that y == 0.5 whenever x <= 0
@implies(m, (x <= 0) => (y == 0.5))
@objective(m, Min, 4x + y)

# Set up the indicator variables for the case that x == -1. Note that this does *not*
# fix x to -1 in the optimization. It simply requires that any implications which depend
# on x will be fixed to the set containing x == -1. In this case, that means that x will
# be fixed to be less than or equal to 0 AND (by the implication above) y will be fixed 
# to be equal to 0.5
setvalue(x, -1)
warmstart!(m, true)
solve(m)

@assert getvalue(x)  -1
@assert getvalue(y)  0.5

Disjunctions

This package also provides the @ifelse macro to create simple if statements which work both on numbers and on optimization variables. For example, let's write a simple update function:

function update(x)
    if x <= 0
        1
    else
        -1
    end
end

This works on numbers:

julia> update(0.5)
-1

but not on variables:

julia> m = Model();

julia> @variable m x;

julia> y = update(x)
ERROR: MethodError: no method matching isless(::Int64, ::JuMP.Variable)
Closest candidates are:
  isless(::Real, ::AbstractFloat) at operators.jl:97
  isless(::Real, ::ForwardDiff.Dual) at /home/rdeits/.julia/v0.6/ForwardDiff/src/dual.jl:161
  isless(::Real, ::Real) at operators.jl:266
Stacktrace:
 [1] update(::JuMP.Variable) at ./REPL[3]:2

But if we replace the if statement with @ifelse, then the function will magically just work in both cases:

function update(x)
    @ifelse(x <= 0, 
      1,
      -1
      )
end

The @? macro is necessary because JuMP does not define <= for its Variable type, and we don't want to commit type piracy by defining it ourselves.

julia> update(0.5)
-1

julia> m = Model();

julia> @variable m -5 <= x <= 5;

julia> y = update(x)
y

Using this, we can easily write optimizations over repeated applications of the update() function, which is something we might want to do in a model-predictive control application:

m = Model(solver=CbcSolver())
@variable m -0.5 <= x <= 0.5

ys = [x]
for i in 1:3
    push!(ys, update(ys[end]))
end

@objective m Max sum(ys)
solve(m)
@assert getvalue.(ys)  [0, 1, -1, 1]

The optimal solution is [0, 1, -1, 1] because our objective is to maximize the sum of the variables in ys. If x were >= 0, then our update rule would require the solution to look like [x, -1, 1, -1], which, due to the limits on 0.5 <= x <= 0.5 would have a sub-optimal objective value. So the indicator constraints have indeed given us the optimal solution.

More Complicated Disjunctions

If your conditional statement can't be expressed as something in the form if x then y else z, then you can use the @switch macro to explicitly state each case:

y = @switch(
    (x <= 0) => 5,
    ((x >= 0) & (x <= 1)) => 6,
    (x >= 1) => 7
)

Note that by using @switch, you are promising that the set of cases you are providing completely cover the feasible set. That is, if you write:

y = @switch(
    (x <= -1) => 5,
    (x >= 1) => 6
)

then x must either be <= -1 or >= 1.

Complementarity and Disjunctions

A final type of conditional you might want to express is a disjunction, which simply says "exactly one of these conditions holds". The @disjunction macro handles this case:

m = Model()
@variable m -1 <= x <= 1
@disjunction(m, (x <= -1), (x >= 1)) 

This can also be used to create complementarity constraints, which require that the product of two expressions be equal to zero. If we want to require that y * x == 0, we can instead require that y == 0 or x == 0:

m = Model()
@variable m -1 <= x <= 1
@variable m -1 <= y <= 1
@disjunction(m, x == 0, y == 0)

Implementation Notes

Indicator constraints are currently enforced using a Big-M formulation. This formulation works by transforming the constraint: z == 1 implies x <= 0 into the constraint:

x <= 0 + M * (1 - z)

where z is restricted to be either 0 or 1.

If M is sufficiently large, then when z == 0, x is essentially unbounded. But when z == 1, the constraint reduces to x <= 0 as desired. The key to successfully applying this formulation is choosing the right value of M. Too small an M will restrict x even when z == 0. Too large a value will create numerical difficulties in the solver.

ConditionalJuMP.jl uses IntervalArithmetic.jl to solve for an appropriate value of M automatically. The idea is that if we know the bounds on x (from the upper and lower bounds in the JuMP model), we can compute exactly how large M needs to be. Even more complicated expressions like z == 1 implies (2x + 3y + z - 2 <= 5x) can be handled automatically because IntervalArithmetic.jl already knows how to propagate intervals through various functions.

As an example, let's look back at the first model:

m = Model(solver=CbcSolver())
@variable(m, -1 <= x <= 1)  # having bounds on all variables is currently a requirement
@variable(m, -1 <= y <= 1)
# Require that y == 0.5 whenever x <= 0
@implies(m, (x <= 0) => (y == 0.5))
@objective(m, Min, 4x + y)

The constraints which are generated for the indicator variable z are:

x + z <= 1
y + 0.5z <= 1
y - 1.5z >= -1
-x - z <= 0

so the sufficiently-big values of M are all in the range [0.5, 1.5], which is certainly small enough not to create numerical problems.

conditionaljump.jl's People

Contributors

doorisajar avatar ianfiske avatar juliatagbot avatar rdeits avatar vtjeng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

conditionaljump.jl's Issues

Idea: v3

Changes to be implemented:

  • get rid of setup_indicators!() and try to go back to just setting up constraints as they are added
    • the problem is that setup_indicators() takes the JuMP.colVals hostage, so it's hard to solve #9 (i.e. to suggest but not fix an indicator's value)
  • instead, create a fix_indicators!() function that just goes through and checks each indicator, fixing it to 1 or 0 as necessary. This means we're back to relying on the user calling setvalue(), but that's OK: that's what that function is for. And now there's no weirdness about whether you set the values and then construct the indicators or the other way around, since the fixing is the only critical part and that's still in a separate function
    • probably have to run the iterator checks in a loop, since e.g. fixing implication[5] might cause implication[4] to have a known value (e.g. if the user added MPC constraints backwards in time)
  • create a hint_indicators!() function that goes through each implication and does a setvalue() (but not a JuMP.fix()) on the binary variable if the conditional can be evaluated

Actually, all of the above can just be replaced with:

  • add implication constraints immediately when constructed
  • change setup_indicators!() to not take any values, but just a single fix::Bool argument
    • it should loop through every indicator in the model, checking the conditional and either setting or fixing the resulting binary variable. It may have to loop through the indicators multiple times until no additional conditionals can be evaluated
    • as a side bonus, a user who forgets to call setup_indicators!() will still get the right answer, just with a possibly slower MIQP
    • this will also magically solve #9 since if the user calls setvalue() on the variables of interest, the binary variables will also get set

In addition, we need to:

  • error immediately when a user tries to do @implies(m, c, v) if we can't evaluate the complement of c. Instead, we need to require that they do @implies(m, c1=>v, c1_complement=>nothing)

Cool package/naming

Very cool that you've made this, it's something we've been thinking about for a long time. I have to ask you to put a disclaimer in the README that this package is not developed or maintained by the JuMP developers, since the name might suggest otherwise. When you're ready for the attention, we'd be happy to list it at http://www.juliaopt.org/packages/.

@JuliaOpt

Better ReLU formulation

As pointed out by @vtjeng, doing

y = @switch((x <= 0) => 0, (x >= 0) => x) 

results in a model whose convex relaxation isn't as tight as it should be (specifically, it misses the constraints that y >= 0 and y >= x). Can we do better?

I think the ideal result would be:

y >= x
y >= 0
y <= x + M(1 - z)
y <= Mz

Really like this feature, but it doesn't seem to work for me.

I'm trying to use the conditional @Implies and it seems that it's not working quite right.
I have two variables declared (with lower and upper bounds) as follows:

Assets = range(1,1,nAssets);
Tasks = range(1,1,nTasks);
MAX_LB = -1000; # These are arbitrary. I wouldn't declare these if ConditionalJuMP didn't require them
MAX_UB = 1000;
@variables m begin
  MAX_LB <= X[Assets,Tasks] <= MAX_UB, Int
  MAX_LB <= XPrime[Assets,Tasks] <= MAX_UB, Int
end
for as in Assets, ts in Tasks
  @implies(m,(XPrime[as,ts] <= -1) => (X[as,ts] == 0))
  @implies(m,(XPrime[as,ts] >= 0) => (X[as,ts] == XPrime[as,ts]))
end

Then when I print the resulting values, all are set to -1000, instead of whatever else I might expect.
Any idea if this is an indication of something else in my code or something wrong with the way I'm using your syntax? I really like this, by the way. All of the proprietary solvers have similar conditionals, and this is a great addition to JuMP.

Make a release with Julia 1.0 compatibility

Now that we have merged Julia 1.0 compatibility into master, are there any outstanding items that prevent a release? Installation of this package under Julia 1.0 has a bit of extra friction until such a release is made. Thanks!

Intercept and replace symbols rather than function calls

instead of turning x + y into _conditional(+, x, y), we could transform it into Var(x) + Var(y) and add appropriate overloads to the Var wrapper type. That would avoid the need to intercept and transform broadcasting operations, since we could just implement (<=)(x::Var, y::Var) = Conditional(<=, x, y) and get [x, x] .<= [y, y] for free.

Vestigial references to @disjunction remain

This is coming from the peanut gallery, but hopefully it's useful 🥜 😄

Despite #5 having merged, there are still references to @disjunction in README.md and src/ConditionalJump.jl.

Try to propagate bounds in equalities

This results in an error message, even though we could reason that -2 <= z <= 6.

@variable(m, -1 <= x <= 1)
@variable(m, 3 <= y <= 5)
@variable(m, z)
@constraint(m, x+y == z)
u = @switch(
    (z <= 0) => 0,
    (z >= 0) => 1,
)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.