Constraint Programming Solver

The approach

The implementation follows the ideas described in Chapter 12, "Concepts, Techniques, and Models of Computer Programming" by Peter Van Roy and Seif Haridi.

An overview of CP implementation in Mozart/Oz.

Status

Proof of concept. Not suitable for use in production. Significant API changes and core implementation rewrites are expected.

Intro

Run in Livebook

Implemented constraints

  • not_equal
  • less_or_equal
  • all_different (decomposition to not_equal)
  • sum

Features

  • views (linear combinations of variables in constraints)
  • solving for satisfaction (CSP) and optimization (COP)
  • distributed solving

Installation

The package can be installed by adding fixpoint to your list of dependencies in mix.exs:

def deps do
  [
    {:fixpoint, "~> 0.7.3"}
  ]
end

Usage

Getting started

Let's solve the following constraint satisfaction problem:

Given two sets of values

x = {1,2}, y = {0, 1}

, find all solutions such that x != y

First step is to create a model that describes the problem we want to solve. The model consists of variables and constraints over the variables. In this example, we have 2 variables $x$ and $y$ and a single constraint $x$ $\neq$ $y$

alias CPSolver.IntVariable
alias CPSolver.Constraint.NotEqual
alias CPSolver.Model
## Variable constructor takes a domain (i.e., set of values), and optional parameters, such as `name`
x = IntVariable.new([1, 2], name: "x")
y = IntVariable.new([0, 1], name: "y")
## Create NotEqual constraint
neq_constraint =  NotEqual.new(x, y)

Now create an instance of CPSolver.Model:

model = Model.new([x, y], [neq_constraint])

Once we have a model, we pass it to CPSolver.solve/1,2.

We can either solve asynchronously:

## Asynchronous solving doesn't block 
{:ok, solver} = CPSolver.solve(model)
Process.sleep(10)
## We can check for solutions and solver state and/or stats,
## for instance:
## There are 3 solutions: {x = 1, y = 0}, {x = 2,  y = 0}, {x = 2, y = 1} 
iex(46)> CPSolver.solutions(solver)
[[1, 0], [2, 0], [2, 1]]

## Solver reports it has found all solutions    
iex(47)> CPSolver.status(solver)
:all_solutions 

## Some stats
iex(48)> CPSolver.statistics(solver)
%{
  elapsed_time: 2472,
  solution_count: 3,
  active_node_count: 0,
  failure_count: 0,
  node_count: 5
}

, or use a blocking call:

iex(49)> {:ok, results} = CPSolver.solve_sync(model)
{:ok,
 %{
   status: :all_solutions,
   statistics: %{
     elapsed_time: 3910,
     solution_count: 3,
     active_node_count: 0,
     failure_count: 0,
     node_count: 5
   },
   variables: ["x", "y"],
   objective: nil,
   solutions: [[2, 1], [1, 0], [2, 0]]
 }}

API

#################
# Solving       
#################
# 
# Asynchronous solving.
# Takes CPSolver.Model instance and solver options as a Keyword. 
# Creates a solver process which runs asynchronously
# and could be controlled and queried for produced solutions and/or status as it runs.
# The solver process is alive even after the solving is completed.
# It's a responsibility of a caller to shut it down
  
{:ok, solver} = CPSolver.solve(model, solver_opts)

# Synchronous solving.
# Starts the solver and gets the results (solutions and/or solver stats) once the solver finishes.
{:ok, solver_results} = CPSolver.solve_sync(model, solver_opts)

, where

Model specification

Configuring solver

Distributed solving

Fixpoint allows to solve an instance of CSP/COP problem using multiple cluster nodes.

Note: Fixpoint will not configure the cluster nodes! It's assumed that each node has the cluster membership and the fixpoint dependency is installed on it. The solving starts on a 'leader' node, and then the work is distributed across participating nodes. The 'leader' node coordinates the process of solving through shared solver state.

Let's collect all solutions for 8-Queens problem using distributed solving.

For demonstration purposes, we will spawn peer nodes like so:

iex --name leader --cookie solver -S mix
### Let's spawn 2 worker nodes...
worker_nodes = Enum.map(["node1", "node2"], fn node -> 
  {:ok, _pid, node_name} = :peer.start(%{name: node, longnames: true, args: ['-setcookie', 'solver']})
  :erpc.call(node_name, :code, :add_paths, [:code.get_path()])
  node_name
end)

Then we'll pass spawned worker nodes to the solver:

## To convince ourselves that the solving runs on worker nodes, we'll use a solution handler:
solution_handler = fn solution -> IO.puts("#{inspect Enum.map(solution, fn {_name, solution} -> solution end)} <- #{inspect Node.self()}") end 
{:ok, _solver} = CPSolver.solve(CPSolver.Examples.Queens.model(8), 
  distributed: worker_nodes, 
  solution_handler: solution_handler)

Examples

Reindeer Ordering

Shows how to put together a model that solves a simple riddle.

N-Queens

Classical N-Queens problem

Sudoku

No explanation needed :-)

SEND+MORE=MONEY

Cryptoarithmetics problem - a riddle that involves arithmetics.

Knapsack

Constraint Optimization Problem - packing items so they fit the knapsack and maximize the total value. Think Indiana Jones trying to fill his backpack with treasures in the best way possible :-)