├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── csp-solver.gemspec ├── lib ├── csp.rb └── csp │ ├── solver.rb │ └── solver │ ├── convenient_constraints.rb │ └── version.rb └── spec ├── csp ├── solver_spec.rb └── support │ ├── mathdoku.rb │ └── sudoku.rb ├── readme_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.2 5 | before_install: gem install bundler -v 1.12.5 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at moore.niemi@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in csp-solver.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matthew Barry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSP::Solver 2 | A gem for solving arbitrary [constraint satisfaction problems][wiki-csp] (CSPs). If the constraints are [hard][hard-constraints] (as opposed to flexible/soft) and can be specified in the Ruby programming language, then this library can find a solution. 3 | 4 | ## Installation 5 | 6 | Add this line to your application's Gemfile: 7 | 8 | ```ruby 9 | gem 'csp-solver' 10 | ``` 11 | 12 | And then execute: 13 | 14 | $ bundle 15 | 16 | Or install it yourself as: 17 | 18 | $ gem install csp-solver 19 | 20 | ## Usage 21 | 22 | ```ruby 23 | require 'csp' 24 | # first we need to set up a Problem 25 | problem = CSP::Solver::Problem.new 26 | # then we need to set up some variables and their domain 27 | weekdays = %i(monday tuesday wednesday thursday friday saturday sunday) 28 | meals = %w(bread cheese potatoes) 29 | problem.vars weekdays, meals 30 | # then we set up constraints as predicate blocks 31 | problem.all_pairs(weekdays) { |a, b| a != b} 32 | # (or you can use the convenience method `all_different`) 33 | # problem.all_different(weekdays) 34 | # then it's as simple as calling #solve 35 | problem.solve 36 | # which returns to us a hash of variables as keys, and 37 | # domain entries as values 38 | # => { weekday: meal } 39 | ``` 40 | 41 | See also: [Official documentation and API][csp-solver-docs] 42 | 43 | ## Development 44 | 45 | This project makes uses [git-flow][] conventions: the **master** branch is intended for the current stable release. Active development commits should be made on the **develop** branch or, better yet, on **feature/** branches that will merge back into **develop**. 46 | 47 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 48 | 49 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number (`CSP::Solver::VERSION`) in `lib/csp/solver/version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org][]. 50 | 51 | ## Contributing 52 | 53 | Bug reports and pull requests are welcome on GitHub at https://github.com/komputerwhiz/csp-solver. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant][] code of conduct. 54 | 55 | ## License 56 | 57 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 58 | 59 | [wiki-csp]: http://en.wikipedia.org/wiki/Constraint_satisfaction_problem 60 | [hard-constraints]: https://en.wikipedia.org/wiki/Constraint_satisfaction_problem#Flexible_CSPs 61 | [csp-solver-docs]: http://komputerwiz.net/apps/csp-solver 62 | [git-flow]: http://nvie.com/posts/a-successful-git-branching-model/ 63 | [rubygems.org]: https://rubygems.org 64 | [Contributor Covenant]: http://contributor-covenant.org 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'csp/solver' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | require 'pry' 11 | Pry.start 12 | 13 | # require 'irb' 14 | # IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /csp-solver.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'csp/solver/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'csp-solver' 8 | spec.version = CSP::Solver::VERSION 9 | spec.authors = ['Matthew Barry', 'Alex Moore-Niemi'] 10 | 11 | spec.summary = 'Solve constraint satisfaction problems with ease.' 12 | spec.description = 'A ruby library for solving arbitrary constraint satisfaction problems. If the constraints can be specified in the ruby programming language, then this library can find a solution.' 13 | spec.homepage = 'https://github.com/komputerwiz/csp-solver' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = 'exe' 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.12' 22 | spec.add_development_dependency 'rake', '~> 10.0' 23 | spec.add_development_dependency 'rspec', '~> 3.0' 24 | spec.add_development_dependency 'pry', '~> 0.10.4' 25 | end 26 | -------------------------------------------------------------------------------- /lib/csp.rb: -------------------------------------------------------------------------------- 1 | require 'csp/solver' 2 | -------------------------------------------------------------------------------- /lib/csp/solver.rb: -------------------------------------------------------------------------------- 1 | require 'csp/solver/version' 2 | require 'csp/solver/convenient_constraints' 3 | require 'securerandom' 4 | 5 | module CSP 6 | module Solver 7 | # You initialize a problem, set its variables and constraints, then 8 | # can call `solve` on it. 9 | class Problem 10 | include ConvenientConstraints 11 | 12 | def initialize 13 | @vars = {} 14 | @gen_prefix = '__gen__' 15 | 16 | @unary_constraints = Hash.new { |h, k| h[k] = [] } 17 | @binary_constraints = Hash.new { |h, k| h[k] = [] } 18 | end 19 | 20 | # backtracking algorithm with interleaved local constraint propagation 21 | def solve(domains = @vars) 22 | # deep-copy domains to prevent changes from "leaking" to outside 23 | domains = domains.each_with_object({}) { |(k, v), ds| ds[k] = v.dup; ds } 24 | 25 | return false unless ac3(domains) 26 | 27 | if domains.values.all? { |dom| dom.size == 1 } 28 | return domains.each_with_object({}) do |(k, v), sol| 29 | sol[k] = v[0] unless k.is_a?(String) && k.start_with?(@gen_prefix) 30 | sol 31 | end 32 | end 33 | 34 | var = domains.select { |_var, dom| dom.size > 1 }.keys.sort_by { |k| domains[k].size }.first 35 | dom = domains[var] 36 | 37 | dom.each do |value| 38 | domains[var] = [value] 39 | 40 | result = solve(domains) 41 | return result if result 42 | end 43 | 44 | domains[var] = dom 45 | 46 | false 47 | end 48 | 49 | def var(id, domain) 50 | @vars[id] = domain.to_a 51 | end 52 | 53 | def vars(ids, domain) 54 | ids.each { |id| var(id, domain.dup) } 55 | end 56 | 57 | def assign(hash) 58 | hash.each { |x, v| @vars[x] = [v] } 59 | end 60 | 61 | def constrain(*vars, &pred) 62 | case vars.length 63 | when 1 64 | unary_constrain(vars[0], &pred) 65 | when 2 66 | binary_constrain(vars[0], vars[1], &pred) 67 | else 68 | nary_constrain(vars, &pred) 69 | end 70 | end 71 | 72 | private 73 | 74 | def ac3(vars) 75 | vars.each do |var, domain| 76 | next unless @unary_constraints.key? var 77 | domain.reject! { |x| !@unary_constraints[var].all? { |c| c.call(x) } } 78 | return false if domain.empty? 79 | end 80 | 81 | worklist = @binary_constraints.keys 82 | 83 | until worklist.empty? 84 | x, y = worklist.shift 85 | reduced_domain = vars[x].reject! { |vx| !vars[y].any? { |vy| @binary_constraints[[x, y]].all? { |c| c.call(vx, vy) } } } 86 | unless reduced_domain.nil? 87 | return false if vars[x].empty? 88 | worklist |= @binary_constraints.keys.select { |k| k.include?(x) && !k.include?(y) } 89 | end 90 | end 91 | 92 | true 93 | end 94 | 95 | def unary_constrain(x, &pred) 96 | raise "No variable #{x}" unless @vars.key? x 97 | 98 | @unary_constraints[x] << pred 99 | end 100 | 101 | def binary_constrain(x1, x2, &pred) 102 | raise "No variable #{x1}" unless @vars.key? x1 103 | raise "No variable #{x2}" unless @vars.key? x2 104 | 105 | @binary_constraints[[x1, x2]] << pred 106 | 107 | # note the swapped parameter order 108 | @binary_constraints[[x2, x1]] << proc { |v2, v1| yield(v1, v2) } 109 | end 110 | 111 | def nary_constrain(vars) 112 | vars.each do |x| 113 | raise "No variable #{x}" unless @vars.key? x 114 | end 115 | 116 | # Reduce n-way constraint to binary constraints: 117 | # Generate auxiliary variable to serve as binary intermediary between all constrained variables. 118 | # This variable's full domain is the cartesian product of all constrained variables' domains, 119 | # but here we filter the domain to only the values that satisfy the constraint. 120 | head, *tail = vars.map { |x| @vars[x] } 121 | dom = head.product(*tail).select { |tuple| yield(*tuple) } 122 | gen_id = @gen_prefix + SecureRandom.uuid 123 | 124 | var(gen_id, dom) 125 | 126 | vars.each_with_index do |x, i| 127 | binary_constrain(x, gen_id) { |v, tuple| v == tuple[i] } 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/csp/solver/convenient_constraints.rb: -------------------------------------------------------------------------------- 1 | module CSP 2 | module Solver 3 | # These methods are convenience methods to allow you 4 | # to set common constraints (predicates) on your Problem. 5 | module ConvenientConstraints 6 | def all_pairs(vars, &block) 7 | pairs = vars.repeated_combination(2).reject { |x, y| x == y } 8 | pairs.each { |x, y| constrain(x, y, &block) } 9 | end 10 | 11 | def all_same(vars) 12 | all_pairs(vars) { |x, y| x == y } 13 | end 14 | 15 | def all_different(vars) 16 | all_pairs(vars) { |x, y| x != y } 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/csp/solver/version.rb: -------------------------------------------------------------------------------- 1 | module CSP 2 | module Solver 3 | VERSION = '0.1.0'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/csp/solver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CSP::Solver do 4 | it 'has a version number' do 5 | expect(CSP::Solver::VERSION).not_to be nil 6 | end 7 | 8 | describe 'what do you want for dinner?' do 9 | let(:csp) { CSP::Solver::Problem.new } 10 | 11 | let(:meals) do 12 | ['red sauce', 13 | 'burritos', 14 | 'shwarma', 15 | 'mushroom sauce', 16 | 'pizza', 17 | 'chinese', 18 | 'dogwood'].freeze 19 | end 20 | 21 | let(:takeout) do 22 | %w(dogwood chinese shwarma burritos) 23 | end 24 | 25 | let(:weekdays) do 26 | %i(monday tuesday 27 | wednesday thursday 28 | friday saturday sunday) 29 | end 30 | 31 | it 'no repeated meals' do 32 | csp.vars weekdays, meals 33 | # could also use #all_different here 34 | # https://komputerwiz.net/apps/csp-solver#api-csp-all_different 35 | # but this just helps expose the predicate 36 | csp.all_pairs(weekdays) { |a, b| a != b } 37 | 38 | solution = csp.solve 39 | 40 | expect(solution.values).to match_array(solution.values.uniq) 41 | end 42 | it 'only takeout on weekends' do 43 | csp.vars weekdays, meals.shuffle 44 | csp.all_pairs(weekdays) { |a, b| a != b } 45 | csp.constrain(:saturday) { |d| takeout.include?(d) } 46 | csp.constrain(:sunday) { |d| takeout.include?(d) } 47 | 48 | plan = csp.solve 49 | 50 | expect(takeout).to include(plan[:saturday]) 51 | expect(takeout).to include(plan[:sunday]) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/csp/support/mathdoku.rb: -------------------------------------------------------------------------------- 1 | require 'csp-solver' 2 | 3 | class Mathdoku < CSP::Solver::Problem 4 | def initialize(n) 5 | super() 6 | 7 | @cols = ('A'..('A'.ord + n - 1).chr).to_a 8 | @rows = (1..n).to_a 9 | 10 | vars @cols.product(@rows).map(&:join).map(&:to_sym), 1..n 11 | 12 | @cols.each do |c| 13 | all_different(@rows.map { |r| "#{c}#{r}".to_sym }) 14 | end 15 | 16 | @rows.each do |r| 17 | all_different(@cols.map { |c| "#{c}#{r}".to_sym }) 18 | end 19 | end 20 | 21 | def sum(value, *vars) 22 | constrain(*vars) { |*args| args.inject(:+) == value } 23 | end 24 | 25 | def difference(value, v1, v2) 26 | constrain(v1, v2) { |a, b| a - b == value || b - a == value } 27 | end 28 | 29 | def product(value, *vars) 30 | constrain(*vars) { |*args| args.inject(:*) == value } 31 | end 32 | 33 | def quotient(value, v1, v2) 34 | constrain(v1, v2) { |a, b| a.fdiv(b) == value || b.fdiv(a) == value } 35 | end 36 | 37 | def print!(solution) 38 | @rows.each do |r| 39 | @cols.each do |c| 40 | print solution["#{c}#{r}".to_sym].to_s + ' ' 41 | end 42 | print "\n" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/csp/support/sudoku.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.dirname(__FILE__) 2 | require 'csp-solver' 3 | 4 | class Sudoku < CSP::Solver::Problem 5 | def initialize(n) 6 | super() 7 | 8 | size = n * n 9 | 10 | @cols = ('A'..('A'.ord + size - 1).chr).to_a 11 | @rows = (1..size).to_a 12 | 13 | vars @cols.product(@rows).map(&:join).map(&:to_sym), 1..size 14 | 15 | @cols.each do |c| 16 | all_different @rows.map { |r| "#{c}#{r}".to_sym } 17 | end 18 | 19 | @rows.each do |r| 20 | all_different @cols.map { |c| "#{c}#{r}".to_sym } 21 | end 22 | 23 | @cols.each_slice(n) do |cs| 24 | @rows.each_slice(n) do |rs| 25 | all_different cs.product(rs).map(&:join).map(&:to_sym) 26 | end 27 | end 28 | end 29 | 30 | def print!(solution) 31 | @rows.each do |r| 32 | @cols.each do |c| 33 | print solution["#{c}#{r}".to_sym].to_s + ' ' 34 | end 35 | print "\n" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/readme_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'README.md' do 4 | readme = File.read File.expand_path('../../README.md', __FILE__) 5 | ruby_examples = readme.scan(/^```ruby(.+?)^```/m).flatten 6 | 7 | ruby_examples.each_with_index do |code, i| 8 | describe "example ##{i}" do 9 | it 'executes without failure' do 10 | begin 11 | 12 | eval(code) 13 | 14 | rescue Gem::LoadError => err 15 | # tolerate gem loading failure for our gem 16 | raise unless err.message.include? 'csp-solver' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'csp/solver' 3 | --------------------------------------------------------------------------------