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

Improve support for constraint programming #2227

Closed
18 of 35 tasks
dourouc05 opened this issue Apr 21, 2020 · 65 comments · Fixed by #3635
Closed
18 of 35 tasks

Improve support for constraint programming #2227

dourouc05 opened this issue Apr 21, 2020 · 65 comments · Fixed by #3635

Comments

@dourouc05
Copy link
Contributor

dourouc05 commented Apr 21, 2020

I'm making this new issue to track the progress on CP support in JuMP. (Brief reminder: started with #2014; then, a few changes in #2051 to make more constraints parse.) I'm opening this issue to discuss the progress on this side (maybe this should be marked as 1.0?).

For now, these things have been implemented:

There are many more details about all this in JuCP, including design considerations and next steps. Some of these steps could be made into GSoC projects (dealing with tests, writing new wrappers, mostly), if it's not too late for inclusion.


More complete list of things:

@dourouc05
Copy link
Contributor Author

New PR #2241 that ought to replace #2229 to add more flexibility in rewriting expressions.

@dourouc05
Copy link
Contributor Author

dourouc05 commented Jan 14, 2021

@dourouc05
Copy link
Contributor Author

A version version of the CPLEX CP Optimizer wrapper is being tagged: JuliaRegistries/General#28051. However, it does not meet all guidelines, but that's on purpose (name close to CPLEX.jl, of course; not importable, because the CI environment doesn't have CPLEX; https://github.com/JuliaRegistries/General/pull/28051/checks?check_run_id=1711999279 is unrelated, I think).

@dourouc05
Copy link
Contributor Author

A few notes on the recent developments. CPLEXCP.jl has a version 0.1.0 tagged; it should really be useful in many cases. Very specific features such as tuning the branching process are not available at all, but the modelling side is quite advanced (except where Concert uses complex-typed variables). There are quite a few remaining questions to answer, have a look at jump-dev/MathOptInterface.jl#1253 (more feedback is welcome there). Similarly, ConstraintProgrammingExtensions.jl has a 0.1.0 tagged, and should be useful to implement other solver wrappers (JuliaConstraints/ConstraintProgrammingExtensions.jl#7).

For longer-term issues, I also added a paragraph to highlight the fact that CP could really benefit from the new NLP features, once they are developed: https://docs.google.com/document/d/1SfwFrIhLyzpRdXyHKMFKv_XKeyJ6mi6kvhnUZ2LEEjM/edit#, under Use Cases (from jump-dev/MathOptInterface.jl#846).

@odow odow added this to the 1.x milestone Sep 18, 2021
@odow
Copy link
Member

odow commented Oct 14, 2021

To update this issue:

@odow
Copy link
Member

odow commented Jul 5, 2022

To update this issue:

  • MOI v1.4.0 includes a number of new constraint programming sets
  • MOI v1.6.0 includes a number of new bridges from constraint programming to MILP formulations

So this mainly needs some documentation to advertise the new features.

@odow
Copy link
Member

odow commented Jan 26, 2023

Adding much better documentation here: #3202.

I don't know if we'll ever get to a point of supporting a first-class constraint programming interface. Dealing with logical operators in a functional way seems hard.

The best we could probably do is to support them in the nonlinear interface, and then have something like MiniZinc.jl try to parse them, and error if it encounters some nonlinearity that it doesn't support (like sin).

@chriscoey
Copy link
Contributor

That would be awesome! Would we wait for the new nonlinear interface before trying this?

@odow
Copy link
Member

odow commented Jan 26, 2023

Would we wait for the new nonlinear interface before trying this?

I guess, technically, we could attempt this at present, just by parsing the Julia AST.

I think the main issue is that something like @NLconstraint(model, x || y == 1) parses as x || (y == 1), not (x || y) == 1.

@chriscoey
Copy link
Contributor

The way it parses is the way I'd interpret it actually. We can think of constraints as expressions that must evaluate to true, in which case the above says either x (bool variable) is true, or y (int variable) is 1. We would want to support a new Bool type variable (not Bin) I think.

@odow
Copy link
Member

odow commented Jan 26, 2023

The way it parses is the way I'd interpret it actually

The problem currently is that nonlinear constraints must be one- or two-sided (in)equalities. Let me have a play. I think originally I didn't go down this track because we were considering a fully fledged structure, but maybe a mix of function-in-set for things like all-different, and boolean relations gets us most of the way there.

@chriscoey
Copy link
Contributor

Maybe there could be a new type of constraint, a logical constraint, that is similar to nlconstraint but doesn't have this 1/2 sided inequality restriction.

@chriscoey
Copy link
Contributor

(let's start by dreaming big, and scale back if there's a clear reason why it's too hard 😁)

@odow
Copy link
Member

odow commented Jan 26, 2023

Hah. This was easier than I thought. I'll make a PR.

julia> using JuMP

julia> import MiniZinc

julia> model = Model(() -> MiniZinc.Optimizer{Int}(MiniZinc.Chuffed()))
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: AUTOMATIC
CachingOptimizer state: EMPTY_OPTIMIZER
Solver name: MiniZinc

julia> set_optimizer_attribute(model, "model_filename", "test.mzn")

julia> @variable(model, x, Bin)
x

julia> @variable(model, y, Bin)
y

julia> @constraint(model, [x, y] in MOI.AllDifferent(2))
[x, y]  MathOptInterface.AllDifferent(2)

julia> @NLconstraint(model, (x || y) == true)
(x || y) - 1.0 = 0

julia> @NLconstraint(model, (x && y) == false)
(x && y) - 0.0 = 0

julia> optimize!(model)
Warning: included file "count.mzn" overrides a global constraint file from the standard library. This is deprecated. For a solver-specific redefinition of a global constraint, override "fzn_<global>.mzn" instead.

Warning: included file "global_cardinality_low_up.mzn" overrides a global constraint file from the standard library. This is deprecated. For a solver-specific redefinition of a global constraint, override "fzn_<global>.mzn" instead.


julia> value(x), value(y)
(0.0, 1.0)

julia> print(read("test.mzn", String))
include "alldifferent.mzn";
var bool: x;
var bool: y;
constraint alldifferent([x, y]);
constraint ((x \/ y) - 1) == false;
constraint ((x /\ y) - 0) == false;
solve satisfy;

@odow
Copy link
Member

odow commented Jan 26, 2023

See jump-dev/MiniZinc.jl#21

@odow
Copy link
Member

odow commented Jan 26, 2023

So I'm pretty happy with that PR. It's a little tacky, but we could make it work much nicer if we added something like #3106.

It's also quite a reasonable approach: use the structured function-in-set approach for combinatorial structure like AllDifferent, and use the nonlinear expression graph for a limited set of nonlinear operators (mainly Boolean operators, plus +, -, etc.) We'll loose the ability to rewrite the boolean expressions via bridges, but we might be able to write a layer that transform a ScalarNonlinearFunction that represents a boolean expression into MILP. And that would pave the way for something like a DCP transformation of ScalarNonlinearFunction into conic. (It'd be opt-in by the user instead of automatic, but that's okay.)

@odow
Copy link
Member

odow commented Jan 26, 2023

@blegat
Copy link
Member

blegat commented Jan 26, 2023

We'll loose the ability to rewrite the boolean expressions via bridges, but we might be able to write a layer that transform a ScalarNonlinearFunction that represents a boolean expression into MILP. And that would pave the way for something like a DCP transformation of ScalarNonlinearFunction into conic. (It'd be opt-in by the user instead of automatic, but that's okay.)

Not sure I understand this sentence. Aren't you describing a bridge from ScalarNonliearFunction into structured boolean constraints ? Why does it have to be opt-in and not selected by the bridge hyper-graph ?

@dourouc05
Copy link
Contributor Author

I think the main issue is that something like @NLconstraint(model, x || y == 1) parses as x || (y == 1), not (x || y) == 1.

Given that, in Julia, || only represents the short-circuiting or, I believe it's totally expected and having another type of variable shouldn't help: it would only confuse typical Julia users. However, Julia offers |, the bitwise or: wouldn't that be better?

Another reason why the current parse seems totally fine: what about using || for disjunctions?

For your original code sample, shouldn't it actually mean @NLconstraint(model, x == 1 || y == 1)?

@odow
Copy link
Member

odow commented Jan 26, 2023

julia> dump(:(x || y == 1))
Expr
  head: Symbol ||
  args: Array{Any}((2,))
    1: Symbol x
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol ==
        2: Symbol y
        3: Int64 1

It lowers as x || (y == 1). (x == 1) || (y == 1) is something different:

julia> dump(:(x == 1 || y == 1))
Expr
  head: Symbol ||
  args: Array{Any}((2,))
    1: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol ==
        2: Symbol x
        3: Int64 1
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol ==
        2: Symbol y
        3: Int64 1

The MiniZinc PR is now working for expressions like x || (b && (y < 5)) (none of the brackets are needed, but useful to show precedence.

julia> dump(:(x || (b && (y < 5))))
Expr
  head: Symbol ||
  args: Array{Any}((2,))
    1: Symbol x
    2: Expr
      head: Symbol &&
      args: Array{Any}((2,))
        1: Symbol b
        2: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol <
            2: Symbol y
            3: Int64 5

julia> dump(:(x || (b && y < 5)))
Expr
  head: Symbol ||
  args: Array{Any}((2,))
    1: Symbol x
    2: Expr
      head: Symbol &&
      args: Array{Any}((2,))
        1: Symbol b
        2: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol <
            2: Symbol y
            3: Int64 5

julia> dump(:(x || b && y < 5))
Expr
  head: Symbol ||
  args: Array{Any}((2,))
    1: Symbol x
    2: Expr
      head: Symbol &&
      args: Array{Any}((2,))
        1: Symbol b
        2: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol <
            2: Symbol y
            3: Int64 5

So I think this is a pretty viable approach for solvers wanting to support boolean expressions.

@dourouc05
Copy link
Contributor Author

dourouc05 commented Nov 7, 2023

As far as I can see (I followed the latest developments from afar), there are all the required elements in MOI/JuMP. I can think of quality-of-life improvements, compared to other CP modelling systems, but not really in scope for JuMP.

@odow
Copy link
Member

odow commented Nov 7, 2023

but not really in scope for JuMP.

Yeah. I think I'm going to rule things like x[y] and count(x .== 1) as out-of-scope.

@chriscoey
Copy link
Contributor

Is there a path forward for supporting more advanced CP variable types, e.g. set variables in minizinc: https://www.minizinc.org/doc-2.7.6/en/modelling2.html#set-constraints

@odow
Copy link
Member

odow commented Nov 8, 2023

I don't think so. We ruled out more complicated variable types in MOI: jump-dev/MathOptInterface.jl#1253.

Is the set variable that much more convenient than a vector of binaries?

@chriscoey
Copy link
Contributor

It can allow more natural modeling. And there could be bridges in place to do e.g. the binary vector of variables transformation for solvers that don't support it. Besides MiniZinc, LocalSolver seems to push using these set/list variable types too. There is some ongoing work (currently private, later to be released) to support that solver and those variable types.

@odow
Copy link
Member

odow commented Nov 8, 2023

What syntax do you imagine at the JuMP level? What syntax at the MOI level?

@dourouc05
Copy link
Contributor Author

There is one possibility: add_constrained_variable with a set like Set() (no pun intended). @odow, I think that's what we discussed.

@chriscoey
Copy link
Contributor

the minizinc knapsack example is

enum ITEM;
int: capacity;

array[ITEM] of int: profits;
array[ITEM] of int: weights;

var set of ITEM: knapsack;

constraint sum (i in knapsack) (weights[i]) <= capacity;

solve maximize sum (i in knapsack) (profits[i]) ;

in Julia say we have ITEM = Set([1,2,3]), then maybe @dourouc05's suggestion for the variable definition would be knapsack = add_constrained_variable(m, Set(ITEM)). then can we somehow allow summing over knapsack, like the minizinc model above?

@odow
Copy link
Member

odow commented Nov 8, 2023

At the JuMP level, you could do something like:

model = Model()
@variable(model, x in List(N))

but then what is the return type of LocalSolver.count(x)? x[i]? How can these be used in other constraints?

The LocalSolver API is very nice (I've used it for a few models), but it is quite a large departure from JuMP's Math Programming roots. I think it is out-of-scope.

@odow
Copy link
Member

odow commented Nov 8, 2023

Oops. Looks like we posted over the top of each other. Same question for:

knapsack = add_constrained_variable(m, Set(ITEM)). then can we somehow allow summing over knapsack, like the minizinc model above?

So what is the type of knapsack? It can't be a Vector{VariableIndex} anymore. And things like length(knapsack) return a VariableIndex? Or some sort of expression?

@blegat
Copy link
Member

blegat commented Nov 8, 2023

These are feasible at the JuMP level, you can define at the JuMP level variables that behave like sets, I have done it in https://github.com/blegat/SetProg.jl.
I guess the question is now what should be the low-level MOI solver interface, and that question is independent from the nice syntax you want the users to have.
What are the things the solver can do with a set except summing or counting ? Is there anything lost in representing it as a vector of binary variables ? Then summing become an affine expression and counting is the sum of the variables.
You could have a nice syntax for set variables at the JuMP level but still represent it as a VectorOfVariables-in-ZeroOne at the MOI level.

@odow
Copy link
Member

odow commented Nov 8, 2023

LocalSolver can do lots of things, but the API is very specific to LocalSolver, and isn't a widely used abstraction:

https://www.localsolver.com/docs/last/modelingfeatures/mathematicalmodelingfeatures.html#table-of-available-operators-and-functions

image

I don't think these belong in JuMP.

@dourouc05
Copy link
Contributor Author

JuMP, probably not; MOI should have a way to encode them (so we can access everything a solver has to offer), if these functions are available in other solvers. Then, another package could extend JuMP to provide a convenient way to use them (that's why I started JuCP, although I never really had time to work on that: https://github.com/JuliaConstraints/JuCP.jl).

@chriscoey
Copy link
Contributor

Yeah there are some common variable types between localsolver and MiniZinc that I would love to see MOI support somehow, like set variables. I'm not so concerned about JuMP support. I don't have the answers on how to do it right now but I don't think we should rule it out if we can come up with an abstraction for variables that is powerful and extensible like that of the sets/bridges.

@chriscoey
Copy link
Contributor

chriscoey commented Nov 9, 2023

Like I wonder how far we could get by just using a normal moi variable index for a set variable, add a x in Set(S) constraint, and use SNF to encode the function of the set variable. Maybe that can get us pretty far. Then for getting the value of the variable, we return a Julia Set. I'm sure there will be challenges along the way.

@odow
Copy link
Member

odow commented Nov 9, 2023

I think it breaks a very large number of assumptions about what MOI.VariableIndex is. A pretty core feature of MOI is that $x$ is a vector in $\mathbb{R}^N$.

Things like MOI.get(model, MOI.VariablePrimal(), x) would no longer be type-stable, because it could return a T or a Set{T}. And we wouldn't be able to tell that MOI.add_constraint(model, x, MOI.GreaterThan(zero(T))) doesn't work because x is a Set. There will also be problems, like what is x + y if x is a set?

The answer has to be that x is something like MOI.SetOfVariables, but then integrating that into MOI is very challenging.

There's also slight distinction. MiniZinc and LocalSolver both provide this functionality as modeling languages, not as solvers. But JuMP isn't designed like that, so it's not obvious that we can or should try to interface with them. It's also not obvious exactly how similar or different they are, both conceptually, and practically as they would be implemented in Julia.

Nevertheless, perhaps a pathway forwards:

  1. Someone prototypes a MOI+a few side functions, that let's you add set variables to Minizinc.jl without a formal MOI interface (I'm imagining x = MiniZinc.add_set_variable(model, n), etc)
  2. Someone writes an interface to LocalSolver, then prototypes the same MOI+side function approach to LocalSolver.jl
  3. We look at the similarities and differences, and take a big think before considering changes to MOI.

The first two steps can happen outside of JuMP and MOI, and don't need to be written by the JuMP contributors. (I'd say it would be a high-risk topic for an interested grad student, or requires $ with no concrete deliverables.)

@blegat
Copy link
Member

blegat commented Nov 9, 2023

Looks like they can be implemented using a vector of VariableIndex-in-ZeroOne.
Let S be a fixed set (which does not even need to be communicated to MOI) and n be its cardinality, you create n binary variables and then

  • count: is the sum of them
  • indexOf: simply depends on the order of the elements in S so does not depend on the MOI variables
  • contains: gives the corresponding binary variable
  • partition and disjoint and cover: that could be equivalently defined on two vector of variables
  • at, indexOf, sort: only needed for the JuMP extension, does not involve MOI

It seems to me that you could write a nice

struct Subset{S}
    set::S
    selected::Vector{JuMP.VariableRef}
end

which would be able to implement all these, e.g.,

function Base.in(s::Subset)
    i = findfirst(s.set)
    if isnothing(i)
        return false
    else
        return s.selected[i]
    end
end

This would give the nice abstraction at the MOI level. At the moment, I fail to see what useful structure would be lost by simply giving binary variables.

@odow
Copy link
Member

odow commented Nov 9, 2023

Let S be a fixed set (which does not even need to be communicated to MOI)

This is kinda what I meant by:

There's also slight distinction. MiniZinc and LocalSolver both provide this functionality as modeling languages, not as solvers.

JuMP can implement this at the modeling level, but that doesn't help us interface with the Set variables in MiniZinc or LocalSolver.

It's also the direction that @dourouc05 was going in with JuCP.jl.

It can be a JuMP extension developed externally. It doesn't (and I think, probably shouldn't, at least in the near- to medium-term) need to be developed in JuMP directly.

@chriscoey
Copy link
Contributor

Right, the direction I was pushing for is to allow interfacing with MiniZinc/LocalSolver's common variable types, not building modeling abstractions in JuMP.

@blegat
Copy link
Member

blegat commented Nov 10, 2023

What would the user gain with this interface ? Is that handled more efficiently than a vector of binary variables ?
If it's only part of the interface to allow easy modeling then there is no point in adding anything at the MOI level. If there is some useful structure that can speed up solving then it's useful to add it as an MOI set but it would be useful to see what is this structure.
To add it to MOI you could do something like jump-dev/MathOptInterface.jl#2198
So you rewrite

@variable(model, S in Subsets(1:3))
@variable(model, T in Subsets(1:3))
@constraint(model, 1 in S)
@constraint(model, partition(S, T) || (3 in S) && (2 in T) := true)

as

struct SubsetFamily
   universe
   containments::Vector{Vector{Int}} # containments[i] gives the list of boolean variables to create to represent whether the element belongs to the subset of id `i`
   partitions::Vector{Vector{Int}} # partitions[i] gives the list of subset id and a boolean variable represent whether they are a partition
end
@variable(model, [s1, s3, t2, pST] in SubsetsFamily(1:3, [[1, 3], [2]], [[1, 2]]))
@constraint(model, s1 := true)
@constraint(model, pST || s3 && t2 := true)

So MiniZinc and LocalSolver will need to implement support for adding constrained variables in SubsetsFamily and we can write a JuMP extension that rewrite the first model which is not MOI-compatible into the one with SubsetsFamily which is MOI-compatible.

@odow
Copy link
Member

odow commented Nov 10, 2023

The point is that @chriscoey want's to interface with the native support of MiniZinc and LocalSolver. He doesn't want a reformulation (though we'd probably want that as well, for things like HiGHS).

It's not about what is more efficient.

@chriscoey
Copy link
Contributor

Thanks to everyone for thinking about this and proposing different solutions. @odow is right about what I want at least, though it's not necessarily what others want. It is conceivable to me at least that LocalSolver for example can do smarter things with Set or List variables compared to a reformulated binary IP type model (which loses structure), but I'm not sure what they do under the hood.

@blegat
Copy link
Member

blegat commented Nov 11, 2023

The idea above with SubsetFamily is not a reformulation, it LocaSlolver can recover the user formulation as is

@odow
Copy link
Member

odow commented Nov 12, 2023

I'd rather not rush (even a well considered proposal) to add new sets for this to MOI. It's not a concept that naturally fits into the f(x) in S formulation in the same way that MOI.AllDifferent etc does.

I'll repeat my proposal that the next steps are:

  1. Someone prototypes a MOI+a few side functions, that let's you add set variables to Minizinc.jl without a formal MOI interface (I'm imagining x = MiniZinc.add_set_variable(model, n), etc)
  2. Someone writes an interface to LocalSolver, then prototypes the same MOI+side function approach to LocalSolver.jl
  3. We look at the similarities and differences, and take a big think before considering changes to MOI.

Since no interface currently exists to LocalSolver (and I don't think anyone is planning on implementing it), this seems unlikely to happen anytime soon.

For the purpose of this issue:

Does JuMP need any more support for constraint programming? At this stage, no. But if new sets/features get added to MOI then we might have something concrete. Therefore, I think this issue can be closed.

I'd prefer that we kept issues open only if there are concrete ideas and some tangible path to resolving them.

The most appropriate next step would seem to be opening an issue in MiniZinc.jl to track how to add support for it's additional functionality that is not exposed by MOI, just like it is possible to access solver-specific functionality in Gurobi.jl via the C interface.

@blegat
Copy link
Member

blegat commented Nov 12, 2023

I'd rather not rush (even a well considered proposal) to add new sets for this to MOI. It's not a concept that naturally fits into the f(x) in S formulation in the same way that MOI.AllDifferent etc does.
Someone prototypes a MOI+a few side functions, that let's you add set variables to Minizinc.jl without a formal MOI interface (I'm imagining x = MiniZinc.add_set_variable(model, n), etc)

And the second step could be to make these available from JuMP, e.g., by defining new MOI sets as the SubsetFamily one, but this set can be first defined in MiniZinc.jl (using the natural extensible design of JuMP/MOI). This second step also does not require any changes at JuMP/MOI so I agree there might not be any immediate actionable action on this repo

@odow
Copy link
Member

odow commented Nov 12, 2023

Perhaps to be clear: I don't like the design of SubsetFamily as a substitute for Set and List variables in MiniZinc and LocalSolver. It feels convoluted and the wrong level of abstraction. I'd prefer to see some non-MOI functions added to MiniZinc.

@odow
Copy link
Member

odow commented Nov 22, 2023

I've added this to the agenda for next week's developer call.

@odow
Copy link
Member

odow commented Dec 28, 2023

Monthly developer call agreed to close this issue.

The consensus was that we've done as much as we can in the framework of MOI. We think that there is room/a desire/a need for things like interval variables, but these are best done as a JuMP extension for now. Adding them to MOI would likely require MOI 2.0 and at least an order of magnitude more work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

5 participants