Constraint Satisfaction Problem (CSP) implementation for Ruby. It provides a useful way to solve problems like resource allocation or planning through a set of constraints.
The most common example of usage for CSPs is probably the game Sudoku.
Add this line to your application's Gemfile:
gem 'winston'
And then execute:
$ bundle
Or install it yourself as:
$ gem install winston
The problem consists of three sets of information: Domain, Variables and Constraints. It will try to determine a value from the given domain for each variable that will satisfy all the constraints.
require 'winston'
csp = Winston::CSP.new
csp.add_variable :a, domain: [1, 2, 3, 4, 5, 6, 7, 8, 9]
csp.add_variable :b, domain: [1, 2, 3, 4, 5, 6, 7, 8, 9]
csp.add_variable :c, domain: [1, 2, 3, 4, 5, 6, 7, 8, 9]
csp.add_constraint(:a, :c) { |a, c| a == c * 2 } # 'a' has to be double of 'c'.
csp.add_constraint(:a, :b) { |a, b| a > b } # 'a' has to be greater than 'b'.
csp.add_constraint(:b, :c) { |b, c| b > c } # 'b' has to be greater than 'c'.
csp.add_constraint(:b) { |b| b % 2 == 0 } # 'b' has to be even.
csp.solve
= { a: 6, b: 4, c: 3 }The default solver is backtracking, and you can configure it with heuristics to speed up search on harder problems.
solver = Winston::Solvers::Backtrack.new(
csp,
variable_strategy: :mrv,
value_strategy: :lcv,
forward_checking: true
)
csp.solve(solver)You can also pass your own strategies as lambdas:
custom_var = ->(vars, assignments, csp) { vars.first }
custom_val = ->(values, var, assignments, csp) { values.reverse }
solver = Winston::Solvers::Backtrack.new(
csp,
variable_strategy: custom_var,
value_strategy: custom_val
)Built-in heuristic helpers are also available:
solver = Winston::Solvers::Backtrack.new(
csp,
variable_strategy: Winston::Heuristics.mrv,
value_strategy: Winston::Heuristics.lcv,
forward_checking: Winston::Heuristics.forward_checking
)You can also use a local-search solver (min-conflicts), which is often fast on large problems:
solver = Winston::Solvers::MinConflicts.new(csp, max_steps: 10_000)
csp.solve(solver)Min-conflicts is not complete, so it may return false even if a solution exists.
For stronger pruning, you can use MAC (Maintaining Arc Consistency), which enforces arc consistency during search:
solver = Winston::Solvers::MAC.new(csp, variable_strategy: :mrv, value_strategy: :lcv)
csp.solve(solver)MAC is complete and usually faster than plain backtracking on constrained problems, but can be slower on very small ones.
- Use
:backtrackfor small problems or when you want deterministic depth-first search. - Use
:macfor tighter constraints or when backtracking explores too many dead ends. - Use
:min_conflictsfor large problems where you want speed and can tolerate incompleteness.
You can build problems using a small DSL:
csp = Winston.define do
domain :digits, (1..9).to_a
var :a, domain: :digits
var :b, domain: :digits
var :c, domain: :digits
constraint(:a, :c) { |a, c| a == c * 2 }
constraint(:a, :b) { |a, b| a > b }
constraint(:b, :c) { |b, c| b > c }
constraint(:b) { |b| b.even? }
end
csp.solveYou can select a solver by name from any CSP instance:
csp.solve(:backtrack, variable_strategy: :mrv)
csp.solve(:mac, value_strategy: :lcv)
csp.solve(:min_conflicts, max_steps: 10_000)Named solvers:
:backtrack:mac:min_conflicts
Winston.define { ... } builds and returns a Winston::CSP.
DSL methods:
domain :name, valuesregisters a named domain.var :name, domain: <values or :name>, value: <preset>, &blockadds a variable.constraint(*vars, allow_nil: false) { |*values, assignments| ... }adds a custom constraint.use_constraint :name, *vars, allow_nil: false, **optionsadds a named constraint.
Notes:
- Domains are static. Use constraints for dynamic behavior.
value:presets a variable and is validated before search starts.allow_nil: truelets a constraint run even if some variables are unset.
Named domains reduce repetition and keep variable declarations clean:
Winston.define do
domain :digits, (1..9).to_a
var :a, domain: :digits
var :b, domain: :digits
endBuilt-in named constraints:
:all_different:not_in_list
Winston.define do
var :a, domain: [1, 2]
var :b, domain: [1, 2]
use_constraint :all_different, :a, :b
endRegister custom constraints:
Winston.register_constraint(:all_twos) do |variables, allow_nil, **_options|
Class.new(Winston::Constraint) do
def validate(assignments)
values = values_at(assignments)
values.all? { |v| v == 2 }
end
end.new(variables: variables, allow_nil: allow_nil)
endUse them in the DSL:
Winston.define do
var :a, domain: [1, 2]
var :b, domain: [1, 2]
use_constraint :all_twos, :a, :b
endIt's possible to preset values for variables, and in that case the problem would not try to determine values for it, but it will take those values into account for validating the constraints.
csp.add_variable "my_var", value: "predefined value"And it's also possible to set the domain as Proc so it'd be evaluated on-demand. Domains are static and do not
receive partial assignments; use constraints for dynamic behavior.
csp.add_variable("other_var") { |var_name, csp| [:a, :b, :c ] }
# same as
csp.add_variable("other_var", domain: proc { |var_name, csp| [:a, :b, :c ] })Constraints can be set for specific variables and are evaluated based on the active solver strategy. Global constraints are evaluated for every assignment; some solvers (like MAC) also use constraints to prune domains.
csp.add_constraint(:a) { |a| a > 0 } # positive value
# the last argument passed to the block is always a map of assignments, in other words, the current
# state of the solution
csp.add_constraint(:a) do |a, assignments|
!assignments.reject { |name, value| name == :a }.values.include?(a) #checks if the value is not present on other variables
end
# a global constraint is evaluated for every assignment and the only argument it receives is a
# hash with all current assignments
csp.add_constraint do |assignments|
assignments.values.uniq.size == assignments.keys.size # checks if every value is unique
endConstraints can also be set as their own objects, which is great for reusability.
csp.add_constraint constraint: MyConstraint.new(...)
# ...
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new # built-in constraint, checks if all values are different from each otherrequire 'winston'
csp = Winston::CSP.new
csp.add_variable :a, domain: [1, 2]
csp.add_variable :b, domain: [1, 2]
csp.add_variable :c, domain: [1, 2]
csp.add_constraint constraint: Winston::Constraints::AllDifferent.new
csp.solve
= falseIMPORTANT NOTE: Depending on the number of variables and the size of the domain it can take a long time to test all different possibilities. In that case it's recommended to use heuristics or stronger solvers like MAC to reduce the number of iterations.
Check the folder spec/examples for more usage examples.
The spec/examples/map_coloring_spec.rb example is a good starting point for small graph problems, and it demonstrates
using the MAC solver via csp.solve(:mac, ...).
Run benchmarks with:
rake benchThe benchmarks live in bench/run.rb and compare backtracking, MAC, and min-conflicts on a few sample problems.
Benchmarks are run with a fixed timeout and number of runs to make results comparable.
- Add more named constraints (sum, all_equal, in_range, ...)
- Add additional inference techniques (backjumping, nogood recording, ...)
- Add more solver examples and benchmarks
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request