Giter Club home page Giter Club logo

constraintsolver.jl's Introduction

Build status codecov Docs Docs

ConstraintSolver.jl

Logo

This package aims to be a constraint solver completely written in Julia. The concepts are more or less fully described on my blog OpenSourc.es. There is of course also the general user manual here which explains how to solve your model.

Goals

  • Easily extendable
  • Teaching/Learning about constraint programming

Installation

You can install this julia package using ] add ConstraintSolver or if you want to change code you might want to use ] dev ConstraintSolver.

Example

You can easily use this package with the same modeling package as you might be used to for solving (non)linear problems in Julia: JuMP.jl.

Sudoku

using JuMP

grid = [6 0 2 0 5 0 0 0 0;
        0 0 0 0 0 3 0 4 0;
        0 0 0 0 0 0 0 0 0;
        4 3 0 0 0 8 0 0 0;
        0 1 0 0 0 0 2 0 0;
        0 0 0 0 0 0 7 0 0;
        5 0 0 2 7 0 0 0 0;
        0 0 0 0 0 0 0 8 1;
        0 0 0 6 0 0 0 0 0]

using ConstraintSolver
# define a shorter name ;)
const CS = ConstraintSolver

# creating a constraint solver model and setting ConstraintSolver as the optimizer.
m = Model(CS.Optimizer) 
# define the 81 variables
@variable(m, 1 <= x[1:9,1:9] <= 9, Int)
# set variables if fixed
for r=1:9, c=1:9
    if grid[r,c] != 0
        @constraint(m, x[r,c] == grid[r,c])
    end
end

for rc = 1:9
    @constraint(m, x[rc,:] in CS.AllDifferent())
    @constraint(m, x[:,rc] in CS.AllDifferent())
end

for br=0:2
    for bc=0:2
        @constraint(m, vec(x[br*3+1:(br+1)*3,bc*3+1:(bc+1)*3]) in CS.AllDifferent())
    end
end

optimize!(m)

# retrieve grid
grid = convert.(Int, JuMP.value.(x))

Supported variables and constraints

You can see a list of currently supported constraints in the docs. In general the solver works only with bounded discrete variables and supports these constraints

  • linear constraints
  • all different
  • table
  • indictoar
  • reified
  • boolean

Examples

A list of example problems can be found on the website by Håkan Kjellerstrand.

Blog posts

If you're interested in how the solver works you can checkout my blog opensourc.es. There are currently around 30 blog posts about the constraint solver and a new one is added about once per month.

Notice

I'm a MSc student in computer science so I don't have much knowledge on how constraint programming works but I'm keen to find out ;)

Support

If you find a bug or improvement please open an issue or make a pull request. Additionally if you use the solver regularly or are interested in further development please checkout my Patreon page or click on the support button at the top of this website. ;)

constraintsolver.jl's People

Contributors

azzaare avatar fredrikekre avatar github-actions[bot] avatar matbesancon avatar shiyuez avatar wikunia 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  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  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  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

constraintsolver.jl's Issues

Alldifferent + linear combination

Is there a better way to combine all different with the linear combination constraint?
i.e sum([1:6,1:6,1:6]) == 8 when alldifferent can remove the 6 which is currently not done.

Branching in two branches

Currently for each possibility a new branch is created.
Instead create branches like

  • List of possibilities <= pivot
  • The other half >= pivot+1
    First just the middle of the current range can be chosen.
    The problem with the current solution is that a lot of branches are created if the number of possible values is high but this is often not necessary.

Export functions

export functions which are used by the user like:
add_constraint! and solve!

Solver failure on small problem

Trying to construct an MWE for #82 , I ran into this. A small problem, which I imagine ConstraintSolver should be able to enumerate and solve in it's entirety (smaller than sudoku) seems to take very long and consume all my RAM.

To reproduce:

using JuMP
using ConstraintSolver
using Cbc # for testing

model = Model()


# Variables
@variable(model, 0 <= inclusion[h = 1:3] <= 1, Int);
@variable(model, 0 <= allocations[h = 1:3, a = 1:3] <= 1, Int);
@variable(model, 0 <= days[h = 1:3, a = 1:3] <= 5, Int);

# Constraints
@constraint(model, must_include[h = 1:3],
    sum(allocations[h,a] for a in 1:3) <= inclusion[h]
);
# at least n
@constraint(model, min_hospitals,
    sum(inclusion[h] for h in 1:3) >= 3
);
# every h must be allocated at most one a
@constraint(model, must_visit[h = 1:3],
    sum(allocations[h,a] for a in 1:3) <= 1
);
# every allocated h must have fewer than 5 days of visits per week
@constraint(model, max_visits[h = 1:3],
    sum(days[h, a] for a in 1:3) <= 5 * inclusion[h]
);

benefit = @expression(model,
    sum(days[h,a] * 5
    for h in 1:3, a in 1:3));


@objective(model, Max, benefit);
set_optimizer(model, with_optimizer(Cbc.Optimizer))
optimize!(model) # working
set_optimizer(model, with_optimizer(ConstraintSolver.Optimizer))
optimize!(model) # consumes all RAM without finishing

Status:

pkg> status
    Status `C:\Users\username\Documents\Projects\projectname\Project.toml`
  [336ed68f] CSV v0.5.23
  [9961bab8] Cbc v0.6.6
  [e0e52ebd] ConstraintSolver v0.1.0 #master (https://github.com/Wikunia/ConstraintSolver.jl)
  [a93c6f00] DataFrames v0.20.0
  [4076af6c] JuMP v0.20.1
  [fdbf4ff8] XLSX v0.5.8

Define a variable by an integer set

Currently you need to define a variable using @variable(m, 1 <= x <= 10, Int) but it also should be possible to define it like @variable(m, x in CS.IntegerSet(1:10)) or
@variable(m, x in CS.IntegerSet([1,2,4,5,9]))

Different traversal strategies

  • Depth first search
  • Best first search
  • First depth until feasible solution

Currently only best first search is implemented

Bug probably in bound computation

The maximum stable set of the following should be 2.7.

matrix = [
        0  1  0  1  0  0  1  0  0  1
        1  0  1  0  1  0  0  0  0  0
        0  1  0  1  0  0  0  0  1  0
        1  0  1  0  1  1  0  0  0  0
        0  1  0  1  0  1  1  1  0  0
        0  0  0  1  1  0  1  0  0  0
        1  0  0  0  1  1  0  1  0  0
        0  0  0  0  1  0  1  0  1  0
        0  0  1  0  0  0  0  1  0  1
        1  0  0  0  0  0  0  0  1  0
    ]
    n = 10
    m = Model(CS.Optimizer)
    @variable(m, x[1:n], Bin)
    for i = 1:n, j = i+1:n
        if matrix[i, j] == 1
            @constraint(m, x[i] + x[j] <= 1)
        end
    end
    w = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]
    @objective(m, Max, dot(w, x))
    optimize!(m)
    @assert MOI.get(m, MOI.ObjectiveValue()) ≈ 2.7

It produces this output:

 #Open      #Closed         Incumbent             Best Bound        Time [s]  
================================================================================
     1           1                -                    3.70             1.50    
     3           3               0.90                  3.70             1.93    
     26          28              1.80                  1.70             1.99    

The incumbent should never be better than the best bound.

Try a different splitting strategy

In Killer sudokus it's much faster to split the tree into #value branches instead of 2 by choosing a middle element.
Maybe trying out to split it into two parts one with a single element and one representing the rest is more valuable. This reduces the overhead when having a large number of possibilities but reduces the number of stupid propagation steps which don't do anything useful.

MOI JuMP

Being able to use it as a JuMP solver

Documentation

  • Explain all options of the solver
  • List of all constraint and objective types

docs

  • doc strings for every funtion
  • use Documenter

Table logging

Print out information of the current state as a table for problems that need longer to solve.

Different branching strategies

Currently the next variable is chosen based on number of possible values and how often it failed at that point (bt_infeasible)

  • ABS
  • Weighted degree heuristic (wdeg)
  • dom / wdeg
  • Impact based search

Open for renaming

I'm thinking about renaming the package as it would sound like it's THE package for constraint programming if it gets an actual julia package. Some of you might know that I'm terrible at naming things... And yes currently there is no other constraint solver julia package but nevertheless it should be up for discussion here.
Based on previous help: Do you have some ideas @ccoffrin @shibumi ?
Current ideas:

  • CPS.jl
  • CoPS.jl
  • CoPra.jl
  • ConstraintProgramming.jl by @rschwarz

SolveTime

Measure solving time internally to be able to get the solve time via JuMP and MOI.

Alldifferent bound computation

For the following problem:

# Variables
@variable(model, 1 <= x[1:10] <= 15, Int)

# Constraints
@constraint(model, sum(x[1:5]) >= 10)
@constraint(model, sum(x[6:10]) <= 15)
@constraint(model, x in CS.AllDifferentSet())

@objective(model, Max, sum(x))
optimize!(model)

The AllDifferentSet() only restricts sum(x) <= 105 but it could restrict sum(x[1:5]) to 65.

Ordering of constraints

I assume that there can be a huge speed up if the constraints are ordered before pruning starts.
The ordering should depend on the function (all_different is slower than sum but might prune more). Fewer unfixed values are normally preferred (i.e smaller graph in all_different)

Currently a variable can't be fixed in a constraint

A variable can only be fixed by removing all other possibilities but not directly.
This is the case because we have the first_ptr which doesn't change in reverse_pruning. We need to have two pruning vectors to be able to handle that.

Confusion between values and indices

It might be due to the solver being tested first on Sudoku instances. When testing the solver on problems with non-integer coefficients, some errors occur, which can be fixed by widening the types.

However, in some places of the code, the value and index spaces are mixed up. An example with the has function:

function has(v::CS.Variable, x::Int)
    if x > v.max || x < v.min
        return false
    end
    ind = v.indices[x + v.offset]
    return v.first_ptr <= ind <= v.last_ptr
end

In the first part, x is used as a value, checking if the variable is between min and max. But in the second part, it is used as an index as x + v.offset. Using values and indices interchangeably will lead to bugs later (what if you have infinite, if you have float coefficients, etc).

(n+1)th variable is not a bounded integer

I have a JuMP model where all variables are bounded and type Int (I had some Bin variables, and changed them to be Int with 0 lower bound and 1 upper bound, but that does not help).

If I run v = JuMP.all_variables(model) on my model then v has length 15272.

So the following error message is very strange:

julia> optimize!(model)
ERROR: Each variable must be an integer and bounded. Currently the variable index 15273 doesn't fulfill this requirements.
Stacktrace:
 [1] check_var_bounds at C:\Users\username\.julia\packages\ConstraintSolver\qrqj1\src\MOI_wrapper\variables.jl:23 [inlined]
 [2] optimize!(::ConstraintSolver.Optimizer) at C:\Users\username\.julia\packages\ConstraintSolver\qrqj1\src\MOI_wrapper\MOI_wrapper.jl:79
 [3] optimize!(::MathOptInterface.Bridges.LazyBridgeOptimizer{ConstraintSolver.Optimizer}) at C:\Users\username\.julia\packages\MathOptInterface\izyVf\src\Bridges\bridge_optimizer.jl:239
 [4] optimize!(::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer,MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}) at C:\Users\username\.julia\packages\MathOptInterface\izyVf\src\Utilities\cachingoptimizer.jl:189
 [5] #optimize!#78(::Bool, ::Bool, ::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::Function, ::Model, ::Nothing) at C:\Users\username\.julia\packages\JuMP\MsUSY\src\optimizer_interface.jl:141
 [6] optimize! at C:\Users\colin37\.julia\packages\JuMP\MsUSY\src\optimizer_interface.jl:111 [inlined] (repeats 2 times)
 [7] top-level scope at none:0

I don't understand what I might be doing wrong, so I'm logging this in the hope that my mistake is obvious to someone else.

Logo

My proposal for the package logo.
ConstraintSolver jl logo

FlatZinc Reader

You might want to implement the ability to read FlatZinc models. This would open up a large library of MiniZinc models for testing.

Get all solutions

Instead of returning just the first solution it would be nice to get all of them.
Maybe one by one such that one can call solve again to get the next possible or a different function name.

Logs for visual debugging

Maybe be able to save more logs of how things got pruned step by step which can be implemented in the visual tree representation. Need to be careful that this doesn't slow things down but will be handy for finding out where it can be improved or does something wrong.

Larger test cases

In general larger test cases would be helpful such that overhead on JuMP or simple measurement errors (some other task on the machine) are reduced.
Probably there are some NxN sudoku test cases out there...

Branch split Auto

Currently it splits into smallest value and rest by default.
A better strategy for optimization problems would be to make this dependent on the coefficient of the particular variable and whether it's minimizing or maximizing.
i.e positive coefficient in a maximization problem => branch split on :Biggest

Better bound using the constraints

The best bound can be way too optimistic if we have <= constraints.
I.e

    m = Model(with_optimizer(CS.Optimizer))
    @variable(m, 1 <= x[1:10] <= 9, Int)
    @constraint(m, sum(x) <= 25)
    @constraint(m, sum(x) >= 20)
    weights = [1,2,3,4,5,6,7,8,9,10]
    @objective(m, Max, sum(weights .* x))
    optimize!(m)

The objective is currently only considering one change but by limiting the sum to 25 this can be reduced by looking at the constraints when calculating the best bound.
Otherwise this will take forever...

Disagreement with Cbc.jl on feasibility

Follow up to #83

I extended the example slightly - adding new constraints to make the search space smaller.
Unfortunately it makes it too small - Cbc is able to find the optimal solution, while ConstraintSolver claims the problem is infeasible. They can't both be right.

Updated code:

using JuMP
using ConstraintSolver
using Cbc # for testing

model = Model()


# Variables
@variable(model, 0 <= inclusion[h = 1:3] <= 1, Int);
@variable(model, 0 <= allocations[h = 1:3, a = 1:3] <= 1, Int);
@variable(model, 0 <= days[h = 1:3, a = 1:3] <= 5, Int);

# Constraints
@constraint(model, must_include[h = 1:3],
    sum(allocations[h,a] for a in 1:3) <= inclusion[h]
);
# at least n
@constraint(model, min_hospitals,
    sum(inclusion[h] for h in 1:3) >= 3
);
# every h must be allocated at most one a
@constraint(model, must_visit[h = 1:3],
    sum(allocations[h,a] for a in 1:3) <= 1
);
# every allocated h must have fewer than 5 days of visits per week
@constraint(model, max_visits[h = 1:3],
    sum(days[h, a] for a in 1:3) <= 5 * inclusion[h]
);
@constraint(model, min_visits[h = 1:3],
    2 * inclusion[h] <= sum(days[h, a] for a in 1:3)
);
# for each a, only have days if allocated
@constraint(model, alloc_days[h = 1:3, a = 1:3],
    days[h,a] <= 5*allocations[h,a]
);
# every aa must be allocated to at most 2 h
@constraint(model, max_allocs[a = 1:3],
    sum(allocations[h,a] for h in 1:3) <= 2
);

benefit = @expression(model,
    sum(days[h,a] * 5
    for h in 1:3, a in 1:3));


@objective(model, Max, benefit);
set_optimizer(model, with_optimizer(Cbc.Optimizer))
optimize!(model) # working
termination_status(model) # OPTIMAL::TerminationStatusCode = 1
set_optimizer(model, with_optimizer(ConstraintSolver.Optimizer))
optimize!(model) # consumes all RAM without finishing
termination_status(model) # INFEASIBLE::TerminationStatusCode = 2

Package status:

pkg> status
    Status `C:\Users\colin37\Documents\Projects\IBS HBS Hospital Allocation\Project.toml`
  [336ed68f] CSV v0.5.23
  [9961bab8] Cbc v0.6.6
  [e0e52ebd] ConstraintSolver v0.1.0 #master (https://github.com/Wikunia/ConstraintSolver.jl)
  [a93c6f00] DataFrames v0.20.0
  [4076af6c] JuMP v0.20.1
  [fdbf4ff8] XLSX v0.5.8

Able to add constraints

At the moment it's not really possible to add constraints. This would need to delete all previously feasible but now infeasible solutions. If a prev feasible solution is still feasible it can be returned otherwise a new search needs to be created but prev infeasible solutions are infeasible therefore they can be ignored.

Better <= bounds

Currently bounds are only really computed if we have positive coefficients in the objective and in the constraint.

Better dispatch strategy

As mentioned by @matbesancon it isn't a good idea to have the type Function in constraints like:

mutable struct LinearConstraint{T <: Real} <: Constraint
    idx                 :: Int
    fct                 :: Function
...

because of dynamic dispatching this will be slower. The idea is to have a similar structure to MOI instead such that new constraints provide two functions:
prune_constraint(com, v::MOI.VectorOfVariables, set) for example or
prune_constraint(com, func:MOI.ScalarAffineFunction, set). As well as a
still_feasible(com,func,set, value, index) function in contrast to the current functions:

function all_different(com::CS.CoM, constraint::BasicConstraint; logs = true)
function all_different(com::CoM, constraint::Constraint, value::Int, index::Int)

Remove sorting

Get only the open node that needs to be processed by filtering and minimum or maximum or simply iterating over it once. More memory and time efficient than a full sort. Should be noticeable in graph coloring when more than enough colors are available.

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.