csp-resolver 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8b89688ec5b6907524a3b31a8159a7c78dbe61578ed77e47ddeddfb956777d4
4
+ data.tar.gz: 2791072bb632b2a19151ba5bd60c2ecbdb2923edfd727a0e3ccb6c1f30546ecb
5
+ SHA512:
6
+ metadata.gz: 0ec7e93632f200c4ffc71d702146acfc64e3395c8a77c987f0190dd64f6f2b377d858e3f8cbbb2341b70d8476ce86719a853359d043546eb0e7a8df374a73bf8
7
+ data.tar.gz: a0f7552bd93a53f387052eeba3b8a3cb411671d362e0bbb1524b41d176acd0d842d29d4e1be08e847d3c6b2e33dc5588dedebb1de2932018f57bb3a97b9d4a33
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.5
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ Metrics/BlockLength:
6
+ Exclude:
7
+ - spec/**/*.rb
8
+ Metrics/ClassLength:
9
+ Max: 200
10
+ Metrics/MethodLength:
11
+ Max: 20
12
+ Layout/FirstArrayElementIndentation:
13
+ EnforcedStyle: consistent
14
+ Layout/FirstHashElementIndentation:
15
+ EnforcedStyle: consistent
16
+ Layout/HashAlignment:
17
+ EnforcedLastArgumentHashStyle: always_inspect
18
+ Layout/MultilineMethodCallIndentation:
19
+ EnforcedStyle: indented
20
+ Style/Documentation:
21
+ Enabled: false
22
+ Layout/EndAlignment:
23
+ EnforcedStyleAlignWith: variable
24
+ Style/ObjectThen:
25
+ EnforcedStyle: yield_self
26
+ Naming/FileName:
27
+ Exclude:
28
+ - 'lib/csp-resolver.rb'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024 Rebase, André Benjamim, Gustavo Alberto.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # CSP Resolver
2
+ The `csp-resolver` gem is a powerful tool designed to solve [Constraint Satisfaction Problems](https://en.wikipedia.org/wiki/Constraint_satisfaction_problem) (CSPs), which are mathematical questions defined by strict constraints that must be met. This tool is suitable for a wide range of applications, from scheduling and planning to configuring complex systems.
3
+
4
+ ## Getting Started
5
+ ### Requirements
6
+ **Ruby** >= 2.5.8
7
+
8
+ ### Installing
9
+ You can install using the following command:
10
+ ```bash
11
+ gem install "csp-resolver"
12
+ ```
13
+
14
+ If you prefer using Bundler, add the following line to your Gemfile:
15
+ ```bash
16
+ gem "csp-resolver"
17
+ ```
18
+
19
+ Then install it:
20
+ ```bash
21
+ $ bundle install
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### **Setup a problem to be solved**
27
+ To setup a problem we need to require the gem and initialize the CSP:
28
+
29
+ ```ruby
30
+ require 'csp-resolver'
31
+
32
+ problem = CSP::Problem.new
33
+ ```
34
+
35
+ ### **Adding variables and domains**
36
+
37
+ To add variables and domains you can use the **`add_variable`** method:
38
+
39
+ ```ruby
40
+ variable = 'A'
41
+ domains = %w[red green blue]
42
+
43
+ problem.add_variable(variable, domains: domains)
44
+ ```
45
+
46
+ If some variables share the same domains, you can use **`add_variables`** (plural) for easier setup:
47
+ ```ruby
48
+ variables = %w[B C]
49
+ domains = %w[red green blue]
50
+
51
+ problem.add_variables(variables, domains: domains)
52
+
53
+ # is the same as
54
+ problem.add_variable('B', domains: domains)
55
+ problem.add_variable('C', domains: domains)
56
+ ```
57
+
58
+ ### **Adding constraints**
59
+ There are three ways of adding a constraint: **built-in methods**, **custom block**, or **custom constraint class**.
60
+
61
+ #### **Using built-in methods**
62
+ Setting a list of variables to be unique between them:
63
+ ```ruby
64
+ # A != B != C
65
+ problem.unique(%w[A B])
66
+ problem.unique(%w[A C])
67
+ problem.unique(%w[C B])
68
+
69
+ # same as
70
+ problem.unique(%w[A B C])
71
+ ```
72
+
73
+ Setting all variable assignments to be different:
74
+ ```ruby
75
+ # A != B != C
76
+ # It will consider all variables of CSP automatically
77
+ problem.all_different
78
+ ```
79
+
80
+ #### **Using a custom block**
81
+ You can use the **`add_constraint`** method passing `variables` and a block to create custom validations:
82
+
83
+ ```ruby
84
+ # Set B != C and B != A
85
+ problem.add_constraint(variables: %w[B C A]) { |b, c, a| b != c && b != a }
86
+ ```
87
+
88
+ The block parameters should correspond to the order of the variables provided.
89
+
90
+ #### **Using a custom constraint class**
91
+ To create a custom constraint class it'll need to answer if an assignment satisfies a condition for a group of variables.
92
+
93
+ The easiest way to do this is inheriting from **`CSP::Constraint`**:
94
+
95
+ ```ruby
96
+ class MyCustomConstraint < CSP::Constraint
97
+ end
98
+ ```
99
+
100
+ Now the **`CustomConstraint`** can receive a list of variables which we will use to check if their assigned values conform to the constraint's rule.
101
+
102
+ ```ruby
103
+ variables = %w[A B C]
104
+
105
+ constraint = MyCustomConstraint.new(variables)
106
+
107
+ # It can answer the arity for constraint
108
+ constraint.unary? # => false
109
+ constraint.binary? # => false
110
+ constraint.arity # => 3
111
+ ```
112
+
113
+ ##### **Implementing the constraint rule**
114
+ To determinate if the solution satisfies or not a constraint we need to implement the **`satisfies?`** method. This method receives a hash containing the current variables assignments.
115
+
116
+ ```ruby
117
+ # Variables can't have the color purple
118
+
119
+ class MyCustomConstraint < CSP::Constraint
120
+ def satisfies?(assignment = {})
121
+ # While not all variables for this constraint are assigned,
122
+ # consider that it doesn't violates the constraint.
123
+ return true if variables.all? { |variable| assignment[variable] }
124
+
125
+ variables.all? { |variable| assignment[variable] != 'purple' }
126
+ end
127
+ end
128
+ ```
129
+
130
+ ##### **Adding the constraint to CSP**
131
+ To add the constraint we must instantiate it and pass the object to **`add_constraint`**:
132
+ ```ruby
133
+ problem = CSP::Problem.new
134
+ problem.add_variables(%w[A B C], domains: %w[purple red green blue])
135
+
136
+ # B can't have the color purple
137
+ constraint = MyCustomConstraint.new(%w[B])
138
+
139
+ # Add the B != purple constraint
140
+ problem.add_constraint(constraint)
141
+ ```
142
+
143
+ ##### **The constructor**
144
+
145
+ The default constructor expects to receive an array of variables to apply the constraint.
146
+
147
+ ```ruby
148
+ class CSP::Constraint
149
+ def initialize(variables)
150
+ @variables = variables
151
+ end
152
+ end
153
+ ```
154
+
155
+ But if you need to add other properties besides the variables, you can override the constructor:
156
+
157
+ ```ruby
158
+ # Instead of only purple, now we can choose which color to exclude.
159
+ class MyCustomConstraint < CSP::Constraint
160
+ def initialize(letters:, color:)
161
+ # set letters as the variables
162
+ super(letters)
163
+
164
+ @letters = letters
165
+ @color = color
166
+ end
167
+
168
+ def satisfies?(assignment = {})
169
+ # since letters is the same as variables, we can usem them interchangeably here.
170
+ return true if @letters.all? { |letter| assignment[letter].present? }
171
+
172
+ # we compare with the color set
173
+ @letters.all? { |letter| assignment[letter] != @color }
174
+ end
175
+ end
176
+ ```
177
+
178
+ And now we can use as we see fit:
179
+ ```ruby
180
+ problem = CSP::Problem.new
181
+ problem.add_variables(%w[A B C], domains: %w[purple red green blue])
182
+
183
+ a_cant_be_green = MyCustomConstraint.new(letters: %w[A], color: 'green')
184
+ b_cant_be_purple = MyCustomConstraint.new(letters: %w[B], color: 'purple')
185
+ c_cant_be_blue = MyCustomConstraint.new(letters: %w[C], color: 'blue')
186
+
187
+ problem.add_constraint(a_cant_be_green)
188
+ problem.add_constraint(b_cant_be_purple)
189
+ problem.add_constraint(c_cant_be_blue)
190
+ ```
191
+
192
+ ##### **TL;DR**
193
+ * Inherit from `CSP::Constraint`
194
+ * Implement a `satisfies?(assignment = {})` that returns a boolean
195
+ * Override the initializer if needed, but pass to `super` the constraint's variables
196
+
197
+ ### Solving the problem
198
+ After setting the problem we can search for the solution by calling `solve`:
199
+
200
+ ```ruby
201
+ problem.solve
202
+ # => { 'A' => 'green', 'B' => 'red', 'C' => 'purple' }
203
+ ```
204
+
205
+ ### Full Example:
206
+
207
+ ```ruby
208
+ # Given the letters A-C, pick a color between red, blue, green, and purple for them.
209
+ # Consider the following rules:
210
+ # * Each letters has a unique color
211
+ # * A can't have the color purple
212
+ # * A and C can't have the color red
213
+ # * B can't have the color purple nor green
214
+
215
+ # Create a constraint class
216
+ class MyCustomConstraint < CSP::Constraint
217
+ def initialize(letters:, color:)
218
+ super(letters)
219
+ @letters = letters
220
+ @color = color
221
+ end
222
+
223
+ def satisfies?(assignment = {})
224
+ @letters.all? { |letter| assignment[letter] != @color }
225
+ end
226
+ end
227
+
228
+ # Initialize the problem
229
+ problem = CSP::Problem.new
230
+
231
+ # Define the letters as variables and colors as domains
232
+ variables = %w[A B C]
233
+ domains = %w[purple red green blue]
234
+
235
+ # Create constraints using the custom class
236
+ a_cant_be_purple = MyCustomConstraint.new(letters: %w[A], color: 'purple')
237
+ a_and_c_cant_be_red = MyCustomConstraint.new(letters: %w[A C], color: 'red')
238
+
239
+ # Add variables and domains
240
+ problem.add_variables(variables, domains: domains)
241
+
242
+ # Set the unique color constraint
243
+ problem.all_different
244
+
245
+ # set A != purple constraint
246
+ problem.add_constraint(a_cant_be_purple)
247
+
248
+ # set A != purple && C != purple constraint
249
+ problem.add_constraint(a_and_c_cant_be_red)
250
+
251
+ # set B != purple && B != green constraint
252
+ problem.add_constraint(variables: %w[B]) { |b| b != 'purple' && b != 'green' }
253
+
254
+ # find the solution
255
+ problem.solve
256
+ # => { 'A' => 'green', 'B' => 'red', 'C' => 'purple' }
257
+ ```
258
+
259
+ ## License
260
+ This project is licensed under the [MIT License](MIT-LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: %i[]
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/csp/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'csp-resolver'
7
+ spec.version = CSP::VERSION
8
+ spec.license = 'MIT'
9
+ spec.authors = ['André Benjamim', 'Gustavo Alberto']
10
+ spec.email = ['andre.benjamim@rebase.com.br', 'gustavo.costa@rebase.com.br']
11
+
12
+ spec.summary = 'A Ruby CSP Solver'
13
+ spec.description = 'This Ruby gem solves CSPs using custom constraints'
14
+ spec.homepage = 'https://github.com/Rebase-BR/csp-resolver'
15
+ spec.required_ruby_version = '>= 2.5.8'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/Rebase-BR/csp-resolver'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/csp-resolver'
4
+
5
+ module CSP
6
+ module Examples
7
+ class EventScheduling
8
+ def call
9
+ number_of_events = 3
10
+ number_of_time_slots = 4
11
+
12
+ variables = number_of_events.times.to_a
13
+ domains = number_of_time_slots.times.to_a
14
+
15
+ csp = CSP::Problem.new
16
+ .add_variables(variables, domains: domains)
17
+
18
+ variables.combination(2).each do |events|
19
+ add_constraint(csp, *events)
20
+ end
21
+
22
+ solution = csp.solve
23
+ solution || 'No solution found'
24
+ end
25
+
26
+ def add_constraint(csp, event1, event2)
27
+ csp.add_constraint(OnlyOneConstraint.new(event1, event2))
28
+ end
29
+
30
+ class OnlyOneConstraint < ::CSP::Constraint
31
+ attr_reader :event1, :event2
32
+
33
+ def initialize(event1, event2)
34
+ super([event1, event2])
35
+
36
+ @event1 = event1
37
+ @event2 = event2
38
+ end
39
+
40
+ def satisfies?(assignment)
41
+ return true if variables.any? { |variable| !assignment.key?(variable) }
42
+
43
+ assignment[event1] != assignment[event2]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/csp-resolver'
4
+
5
+ module CSP
6
+ module Examples
7
+ class MapColoring
8
+ def call # rubocop:disable Metrics/MethodLength
9
+ variables = [
10
+ 'Western Australia',
11
+ 'Northern Territory',
12
+ 'South Australia',
13
+ 'Queensland',
14
+ 'New South Wales',
15
+ 'Victoria',
16
+ 'Tasmania'
17
+ ]
18
+
19
+ domains = %w[red blue green]
20
+
21
+ csp = CSP::Problem.new
22
+ .add_variables(variables, domains: domains)
23
+
24
+ add_constraint(csp, 'Western Australia', 'Northern Territory')
25
+ add_constraint(csp, 'Western Australia', 'South Australia')
26
+ add_constraint(csp, 'South Australia', 'Northern Territory')
27
+ add_constraint(csp, 'Queensland', 'Northern Territory')
28
+ add_constraint(csp, 'Queensland', 'South Australia')
29
+ add_constraint(csp, 'Queensland', 'New South Wales')
30
+ add_constraint(csp, 'New South Wales', 'South Australia')
31
+ add_constraint(csp, 'Victoria', 'South Australia')
32
+ add_constraint(csp, 'Victoria', 'New South Wales')
33
+ add_constraint(csp, 'Victoria', 'Tasmania')
34
+
35
+ solution = csp.solve
36
+ solution || 'No solution found'
37
+ end
38
+
39
+ def add_constraint(csp, place1, place2)
40
+ csp.add_constraint(MapColoringConstraint.new(place1, place2))
41
+ end
42
+
43
+ class MapColoringConstraint < ::CSP::Constraint
44
+ attr_reader :place1, :place2
45
+
46
+ def initialize(place1, place2)
47
+ super([place1, place2])
48
+
49
+ @place1 = place1
50
+ @place2 = place2
51
+ end
52
+
53
+ def satisfies?(assignment)
54
+ # If any of them is not assigned then there's no conflict
55
+ return true if variables.any? { |variable| !assignment.key?(variable) }
56
+
57
+ assignment[place1] != assignment[place2]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/examples/queen.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/csp-resolver'
4
+
5
+ module CSP
6
+ module Examples
7
+ class Queen
8
+ def call(queens_number = 8)
9
+ variables = queens_number.times.to_a
10
+
11
+ csp = CSP::Problem.new
12
+ .add_variables(variables, domains: variables)
13
+ .add_constraint(QueensConstraint.new(variables))
14
+ solution = csp.solve
15
+
16
+ solution || 'No solution found'
17
+ end
18
+
19
+ class QueensConstraint < ::CSP::Constraint
20
+ attr_reader :columns
21
+
22
+ def initialize(columns)
23
+ super(columns)
24
+
25
+ @columns = columns
26
+ end
27
+
28
+ def satisfies?(assignment)
29
+ assignment.each do |(queen_col1, queen_row1)|
30
+ (queen_col1 + 1..columns.size).each do |queen_col2|
31
+ next unless assignment.key?(queen_col2)
32
+
33
+ queen_row2 = assignment[queen_col2]
34
+
35
+ return false if queen_row1 == queen_row2
36
+ return false if (queen_row1 - queen_row2).abs == (queen_col1 - queen_col2).abs
37
+ end
38
+ end
39
+
40
+ true
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/csp-resolver'
4
+
5
+ module CSP
6
+ module Examples
7
+ # Three sculptures (A, B, C) are to be exhibited in rooms 1,2 of an art gallery
8
+ #
9
+ # The exhibition must satisfy the following conditions:
10
+ # 1. Sculptures A and B cannot be in the same room
11
+ # 2. Sculptures B and C must be in the same room
12
+ # 3. Room 2 can only hold one sculpture
13
+ class Sculpture
14
+ def call
15
+ variables = %w[A B C]
16
+
17
+ csp = CSP::Problem.new
18
+ .add_variable('A', domains: [1, 2])
19
+ .add_variable('B', domains: [1, 2])
20
+ .add_variable('C', domains: [1, 2])
21
+ .unique(%w[A B])
22
+ .add_constraint(variables: %w[B C]) { |b, c| b == c }
23
+ .add_constraint(RoomLimitToOneConstraint.new(room: 2, variables: variables))
24
+ solution = csp.solve
25
+
26
+ solution || 'No solution found'
27
+ end
28
+
29
+ class CannotBeInSameRoomConstraint < ::CSP::Constraint
30
+ def satisfies?(assignment)
31
+ values = assignment.values_at(*variables)
32
+
33
+ return true if values.any?(&:nil?)
34
+
35
+ values == values.uniq
36
+ end
37
+ end
38
+
39
+ class MustBeInSameRoomConstraint < ::CSP::Constraint
40
+ def satisfies?(assignment)
41
+ values = assignment.values_at(*variables)
42
+
43
+ return true if values.any?(&:nil?)
44
+
45
+ values.uniq.size == 1
46
+ end
47
+ end
48
+
49
+ class RoomLimitToOneConstraint < ::CSP::Constraint
50
+ attr_reader :room
51
+
52
+ def initialize(room:, variables:)
53
+ super(variables)
54
+ @room = room
55
+ end
56
+
57
+ def satisfies?(assignment)
58
+ values = assignment.values_at(*variables)
59
+
60
+ values.count(room) <= 1
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module CSP
6
+ module Algorithms
7
+ class Backtracking
8
+ extend Forwardable
9
+
10
+ ORDERING_ALGORITHM = Ordering::NoOrder
11
+ FILTERING_ALGORITHM = Filtering::NoFilter
12
+ LOOKAHEAD_ALGORITHM = Lookahead::NoAlgorithm
13
+
14
+ attr_reader :problem, :solutions, :max_solutions,
15
+ :ordering_algorithm, :filtering_algorithm, :lookahead_algorithm
16
+
17
+ def_delegators :problem, :variables, :constraints
18
+
19
+ def initialize(
20
+ problem:,
21
+ ordering_algorithm: nil,
22
+ filtering_algorithm: nil,
23
+ lookahead_algorithm: nil,
24
+ max_solutions: 1
25
+ )
26
+ @problem = problem
27
+ @ordering_algorithm = ordering_algorithm || ORDERING_ALGORITHM.new(problem)
28
+ @filtering_algorithm = filtering_algorithm || FILTERING_ALGORITHM.new(problem)
29
+ @lookahead_algorithm = lookahead_algorithm || LOOKAHEAD_ALGORITHM.new(problem)
30
+ @max_solutions = max_solutions
31
+ @solutions = []
32
+ end
33
+
34
+ def backtracking(assignment = {})
35
+ backtracking_recursion(assignment, problem_domains)
36
+ end
37
+
38
+ def consistent?(variable, assignment)
39
+ constraints[variable].all? do |constraint|
40
+ constraint.satisfies?(assignment)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def problem_domains
47
+ problem.domains
48
+ end
49
+
50
+ def backtracking_recursion(assignment, domains)
51
+ return solutions if max_solutions?
52
+ return add_solution(assignment) if complete?(assignment)
53
+
54
+ unassigned = next_unassigned_variable(assignment)
55
+
56
+ domains_for(unassigned, assignment, domains).each do |value|
57
+ local_assignment = assignment.clone
58
+ local_assignment[unassigned] = value
59
+
60
+ next unless consistent?(unassigned, local_assignment)
61
+
62
+ new_domains = lookahead(local_assignment, domains)
63
+
64
+ next unless new_domains
65
+
66
+ backtracking_recursion(local_assignment, new_domains)
67
+
68
+ return solutions if max_solutions?
69
+ end
70
+
71
+ []
72
+ end
73
+
74
+ def add_solution(assignment)
75
+ solutions << assignment
76
+ end
77
+
78
+ def max_solutions?
79
+ solutions.size >= max_solutions
80
+ end
81
+
82
+ def complete?(assignment)
83
+ assignment.size == variables.size
84
+ end
85
+
86
+ def next_unassigned_variable(assignment)
87
+ unassigned_variables(assignment).first
88
+ end
89
+
90
+ def unassigned_variables(assignment)
91
+ variables
92
+ .reject { |variable| assignment.key?(variable) }
93
+ .yield_self { |v| ordering_algorithm.call(v) }
94
+ end
95
+
96
+ def domains_for(unassigned, assignment, domains)
97
+ filtering_algorithm.call(
98
+ values: domains[unassigned],
99
+ assignment_values: assignment.values.flatten
100
+ )
101
+ end
102
+
103
+ def lookahead(assignment, domains)
104
+ lookahead_algorithm.call(
105
+ variables: variables,
106
+ assignment: assignment,
107
+ domains: domains
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ module Algorithms
5
+ module Filtering
6
+ class NoFilter
7
+ attr_reader :problem
8
+
9
+ def self.for(problem:, dependency: nil) # rubocop:disable Lint/UnusedMethodArgument
10
+ new(problem)
11
+ end
12
+
13
+ def initialize(problem)
14
+ @problem = problem
15
+ end
16
+
17
+ def call(values:, assignment_values: []) # rubocop:disable Lint/UnusedMethodArgument
18
+ values
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ module Algorithms
5
+ module Lookahead
6
+ class Ac3
7
+ attr_reader :problem
8
+
9
+ def initialize(problem)
10
+ @problem = problem
11
+ end
12
+
13
+ def call(variables:, assignment:, domains:)
14
+ new_domains = variables.each_with_object({}) do |variable, domains_hash|
15
+ variable_domains = Array(assignment[variable] || domains[variable])
16
+
17
+ domains_hash[variable] = unary_check(variable, variable_domains)
18
+ end
19
+
20
+ variable_arcs = arcs(variables)
21
+
22
+ arc_consistency(variable_arcs, new_domains)
23
+ end
24
+
25
+ def arc_consistency(arcs, domains)
26
+ queue = arcs.dup
27
+
28
+ until queue.empty?
29
+ arc, *queue = queue
30
+ x, y = arc.keys.first
31
+ constraint = arc.values.first
32
+
33
+ next unless arc_reduce(x, y, constraint, domains)
34
+ return nil if domains[x].empty?
35
+
36
+ new_arcs = find_arcs(x, y, arcs)
37
+ queue.push(*new_arcs)
38
+ end
39
+
40
+ domains
41
+ end
42
+
43
+ def arc_reduce(x, y, constraint, domains) # rubocop:disable Naming/MethodParameterName
44
+ changed = false
45
+ x_domains = domains[x]
46
+ y_domains = domains[y]
47
+
48
+ x_domains.each do |x_value|
49
+ consistent = y_domains.any? do |y_value|
50
+ sat = constraint.satisfies?({ x => x_value, y => y_value })
51
+
52
+ sat
53
+ end
54
+
55
+ next if consistent
56
+
57
+ x_domains -= [x_value]
58
+ changed = true
59
+ end
60
+
61
+ domains[x] = x_domains
62
+
63
+ changed
64
+ end
65
+
66
+ # Returns all (z, x) arcs where z != y
67
+ def find_arcs(x, y, arcs) # rubocop:disable Naming/MethodParameterName
68
+ arcs.select do |arc|
69
+ arc.any? do |(first, second), _constraint|
70
+ first != y && second == x
71
+ end
72
+ end
73
+ end
74
+
75
+ # Setup arcs between variables
76
+ def arcs(variables)
77
+ variables.each_with_object([]) do |variable, worklist|
78
+ constraints = problem.constraints[variable].select(&:binary?)
79
+
80
+ constraints.each do |constraint|
81
+ variables_ij = [variable] | constraint.variables # make current variable be the first
82
+
83
+ worklist << { variables_ij => constraint }
84
+ end
85
+ end
86
+ end
87
+
88
+ def unary_check(variable, variable_domains)
89
+ constraints = problem.constraints[variable].select(&:unary?)
90
+
91
+ variable_domains.select do |domain|
92
+ constraints.all? do |constraint|
93
+ constraint.satisfies?({ variable => domain })
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ module Algorithms
5
+ module Lookahead
6
+ class NoAlgorithm
7
+ attr_reader :problem
8
+
9
+ def initialize(problem)
10
+ @problem = problem
11
+ end
12
+
13
+ def call(variables:, assignment:, domains:) # rubocop:disable Lint/UnusedMethodArgument
14
+ domains
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ module Algorithms
5
+ module Ordering
6
+ class NoOrder
7
+ attr_reader :problem
8
+
9
+ def self.for(problem:, dependency: nil) # rubocop:disable Lint/UnusedMethodArgument
10
+ new(problem)
11
+ end
12
+
13
+ def initialize(problem)
14
+ @problem = problem
15
+ end
16
+
17
+ def call(variables)
18
+ variables
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ class Constraint
5
+ attr_reader :variables
6
+
7
+ def initialize(variables = [])
8
+ @variables = variables
9
+ end
10
+
11
+ def satisfies?(_assignment = {})
12
+ raise StandardError, 'Not Implemented. Should return a boolean'
13
+ end
14
+
15
+ def unary?
16
+ arity == 1
17
+ end
18
+
19
+ def binary?
20
+ arity == 2
21
+ end
22
+
23
+ def arity
24
+ @arity ||= variables.size
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ module Constraints
5
+ class AllDifferentConstraint < CSP::Constraint
6
+ def satisfies?(assignment)
7
+ assignment.values == assignment.values.uniq
8
+ end
9
+ end
10
+
11
+ class UniqueConstraint < CSP::Constraint
12
+ def satisfies?(assignment)
13
+ values = assignment.values_at(*variables)
14
+
15
+ return true if values.any?(&:nil?)
16
+
17
+ values == values.uniq
18
+ end
19
+ end
20
+
21
+ class CustomConstraint < CSP::Constraint
22
+ attr_reader :block
23
+
24
+ def initialize(variables, block)
25
+ super(variables)
26
+ @block = block
27
+ end
28
+
29
+ def satisfies?(assignment)
30
+ values = assignment.values_at(*variables)
31
+ return true if values.any?(&:nil?)
32
+
33
+ block.call(*values)
34
+ end
35
+ end
36
+
37
+ def all_different
38
+ add_constraint(AllDifferentConstraint.new(variables))
39
+
40
+ self
41
+ end
42
+
43
+ def unique(variables)
44
+ add_constraint(UniqueConstraint.new(variables))
45
+
46
+ self
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ # TODO: implement dependent factor with weight
5
+ # TODO: implement lookahead, arc-consistency, ac3
6
+ class Problem
7
+ include CSP::Constraints
8
+
9
+ attr_reader :variables, :domains, :constraints, :max_solutions,
10
+ :ordering_algorithm, :filtering_algorithm, :lookahead_algorithm
11
+
12
+ InvalidConstraintVariable = Class.new(StandardError)
13
+ VariableShouldNotBeEmpty = Class.new(StandardError)
14
+ DomainsShouldNotBeEmpty = Class.new(StandardError)
15
+ VariableAlreadySeted = Class.new(StandardError)
16
+
17
+ def initialize(max_solutions: 1)
18
+ @variables = []
19
+ @domains = {}
20
+ @constraints = {}
21
+ @max_solutions = max_solutions
22
+ end
23
+
24
+ def solve(assignment = {})
25
+ Utils::Array.wrap(search_solution(assignment))
26
+ end
27
+
28
+ def add_variable(variable, domains:)
29
+ if (variable.respond_to?(:empty?) && variable.empty?) || variable.nil?
30
+ raise VariableShouldNotBeEmpty, 'Variable was empty in the function parameter'
31
+ end
32
+ raise DomainsShouldNotBeEmpty, 'Domains was empty in the function parameter' if domains.empty?
33
+ raise VariableAlreadySeted, "Variable #{variable} has already been seted" if variables.include?(variable)
34
+
35
+ variables << variable
36
+ @domains[variable] = domains
37
+ constraints[variable] = []
38
+
39
+ self
40
+ end
41
+
42
+ def add_variables(variables, domains:)
43
+ variables.each do |variable|
44
+ add_variable(variable, domains: domains)
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ def add_constraint(constraint = nil, variables: nil, &block)
51
+ validate_parameters(constraint, variables, block)
52
+
53
+ constraint = CustomConstraint.new(variables, block) if block
54
+
55
+ constraint.variables.each do |variable|
56
+ next constraints[variable] << constraint if constraints.include?(variable)
57
+
58
+ raise InvalidConstraintVariable,
59
+ "Constraint's variable doesn't exists in CSP"
60
+ end
61
+
62
+ self
63
+ end
64
+
65
+ def add_ordering(ordering_algorithm)
66
+ @ordering_algorithm = ordering_algorithm
67
+ end
68
+
69
+ def add_filtering(filtering_algorithm)
70
+ @filtering_algorithm = filtering_algorithm
71
+ end
72
+
73
+ def add_lookahead(lookahead_algorithm)
74
+ @lookahead_algorithm = lookahead_algorithm
75
+ end
76
+
77
+ private
78
+
79
+ def validate_parameters(constraint, variables, block)
80
+ if missing_both_constraint_and_block?(constraint, block)
81
+ raise ArgumentError, 'Either constraint or block must be provided'
82
+ end
83
+ if provided_both_constraint_and_block?(constraint, block)
84
+ raise ArgumentError, 'Both constraint and block cannot be provided at the same time'
85
+ end
86
+ if missing_variables_for_block?(block, variables)
87
+ raise ArgumentError, 'Variables must be provided when using a block'
88
+ end
89
+ return unless block_arity_exceeds_variables?(block, variables)
90
+
91
+ raise ArgumentError, 'Block should not have more arity than the quantity of variables'
92
+ end
93
+
94
+ def missing_both_constraint_and_block?(constraint, block)
95
+ constraint.nil? && block.nil?
96
+ end
97
+
98
+ def provided_both_constraint_and_block?(constraint, block)
99
+ constraint && block
100
+ end
101
+
102
+ def missing_variables_for_block?(block, variables)
103
+ block && variables.nil?
104
+ end
105
+
106
+ def block_arity_exceeds_variables?(block, variables)
107
+ !variables.nil? && block.arity > variables.length
108
+ end
109
+
110
+ def search_solution(assignment = {})
111
+ algorithm.backtracking(assignment)
112
+ end
113
+
114
+ def algorithm
115
+ Algorithms::Backtracking.new(
116
+ problem: self,
117
+ ordering_algorithm: ordering_algorithm,
118
+ filtering_algorithm: filtering_algorithm,
119
+ lookahead_algorithm: lookahead_algorithm,
120
+ max_solutions: max_solutions
121
+ )
122
+ end
123
+ end
124
+ end
data/lib/csp/utils.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Utils
4
+ module Array
5
+ def self.wrap(object)
6
+ if object.nil?
7
+ []
8
+ elsif object.respond_to?(:to_ary)
9
+ object.to_ary || [object]
10
+ else
11
+ [object]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSP
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'csp/version'
4
+ require_relative 'csp/constraint'
5
+ require_relative 'csp/utils'
6
+ require_relative 'csp/algorithms/filtering/no_filter'
7
+ require_relative 'csp/algorithms/ordering/no_order'
8
+ require_relative 'csp/algorithms/lookahead/no_algorithm'
9
+ require_relative 'csp/algorithms/lookahead/ac3'
10
+ require_relative 'csp/algorithms/backtracking'
11
+ require_relative 'csp/constraints'
12
+ require_relative 'csp/problem'
13
+
14
+ module CSP; end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: csp-resolver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - André Benjamim
8
+ - Gustavo Alberto
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-05-16 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This Ruby gem solves CSPs using custom constraints
15
+ email:
16
+ - andre.benjamim@rebase.com.br
17
+ - gustavo.costa@rebase.com.br
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rubocop.yml"
23
+ - MIT-LICENSE
24
+ - README.md
25
+ - Rakefile
26
+ - csp-resolver.gemspec
27
+ - examples/event_scheduling.rb
28
+ - examples/map_coloring.rb
29
+ - examples/queen.rb
30
+ - examples/sculpture.rb
31
+ - lib/csp-resolver.rb
32
+ - lib/csp/algorithms/backtracking.rb
33
+ - lib/csp/algorithms/filtering/no_filter.rb
34
+ - lib/csp/algorithms/lookahead/ac3.rb
35
+ - lib/csp/algorithms/lookahead/no_algorithm.rb
36
+ - lib/csp/algorithms/ordering/no_order.rb
37
+ - lib/csp/constraint.rb
38
+ - lib/csp/constraints.rb
39
+ - lib/csp/problem.rb
40
+ - lib/csp/utils.rb
41
+ - lib/csp/version.rb
42
+ homepage: https://github.com/Rebase-BR/csp-resolver
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/Rebase-BR/csp-resolver
47
+ source_code_uri: https://github.com/Rebase-BR/csp-resolver
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.5.8
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.0.3
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: A Ruby CSP Solver
67
+ test_files: []