Skip to content

Commit 765bc74

Browse files
committed
Added Sudoku Tests, MinConflicts solver
1 parent 26f7803 commit 765bc74

File tree

9 files changed

+379
-8
lines changed

9 files changed

+379
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 0.9.9
2+
- Added Sudoku Example Tests
3+
- Added Initial Version of a MinConflicts solver
4+
15
### 0.9.8
26
- LCV Heuristic
37
- Changed to Apache License

Demo/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3636

3737
@IBAction func solve(_ sender: AnyObject) {
3838
//create the CSP
39-
var variables = circuitBoards
39+
let variables = circuitBoards
4040
var domains: Dictionary<CircuitBoard, [(Int, Int)]> = Dictionary<CircuitBoard, [(Int, Int)]>()
4141
for variable in variables {
4242
domains[variable] = variable.generateDomain(boardWidth: boardWidth, boardHeight: boardHeight)

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Linux Compatible](https://img.shields.io/badge/Linux-compatible-4BC51D.svg?style=flat)](https://swift.org)
88
[![Twitter Contact](https://img.shields.io/badge/contact-@davekopec-blue.svg?style=flat)](https://twitter.com/davekopec)
99

10-
SwiftCSP is a constraint satisfaction problem solver written in pure Swift (no Cocoa). It utilizes a simple backtracking algorithm with plans for future optimizations. At this stage of development, it's fairly slow but it includes examples of solving actual problems. It should run on all Swift platforms (iOS, OS X, Linux, tvOS, etc.).
10+
SwiftCSP is a constraint satisfaction problem solver written in pure Swift (no Cocoa). It utilizes a simple backtracking algorithm with optional standard heuristics to improve performance on some problems. At this stage of development, it's fairly slow but it includes examples of solving actual problems. It should run on all Swift platforms (iOS, OS X, Linux, tvOS, etc.).
1111

1212
A [constraint satisfaction problem](https://en.wikipedia.org/wiki/Constraint_satisfaction_problem) is a problem composed of *variables* that have possible values (*domains*) and *constraints* on what those values can be. A solver finds a potential solution to that problem by selecting values from the domains of each variable that fit the constraints. For more information you should checkout Chapter 6 of Artificial Intelligence: A Modern Approach (Third Edition) by Norvig and Russell.
1313

@@ -19,13 +19,14 @@ The unit tests included with the project are also well known toy problems includ
1919
- [The Australian Map Coloring Problem](https://en.wikipedia.org/wiki/Four_color_theorem)
2020
- [Send + More = Money](https://en.wikipedia.org/wiki/Verbal_arithmetic)
2121
- [Eight Queens Problem](https://en.wikipedia.org/wiki/Eight_queens_puzzle)
22+
- [Sudoku](https://en.wikipedia.org/wiki/Sudoku)
2223

2324
Looking at them should give you a good idea about how to use the library. In addition, the program included in the main project is a nice graphical example of the circuit board layout problem (it's also a great example of Cocoa Bindings on macOS).
2425

2526
## Usage
2627
You will need to create an instance of `CSP` and set its `variables` and `domains` at initialization. You will also need to subclass one of `Constraint`'s canonical subclasses: `UnaryConstraint`, `BinaryConstraint`, or `ListConstraint` and implement the `isSatisfied()` method. Then you will need to add instances of your `Constraint` subclass to the `CSP`. All of these classes make use of generics - specifically you should specify the type of the variables and the type of the domains.
2728

28-
To solve your `CSP` you will call the function `backtrackingSearch()`. If your CSP is of any significant size, you will probably want to do this in an asynchronous block or background thread.
29+
To solve your `CSP` you will call the function `backtrackingSearch()`. If your CSP is of any significant size, you will probably want to do this in an asynchronous block or background thread. You can also try the `minConflicts()` solver which is not as mature.
2930

3031
### Example
3132
Once again, I suggest looking at the unit tests, but here's a quick overview of what it's like to setup the eight queens problem:
@@ -53,13 +54,13 @@ final class EightQueensConstraint: ListConstraint <Int, Int>
5354
We therefore have a non-generic subclass of a generic superclass.
5455

5556
## Performance
56-
Performance is currently not great for problems with a medium size domain space. Profiling has shown a large portion of this may be attributable to the performance of Swift's native Dictionary type. Improved heuristics such as MAC3 are planned (spaces in the source code are left for them and contributions are welcome!) and should improve the situation. You can turn on the MRV or LCV heuristics (which are already implemented) when calling `backtrackingSearch()` to improve performance in many instances. In my testing MRV improves many searches, whereas the LCV implementation still leaves something to be desired, but may be useful in very specific problems.
57+
Performance is currently not great for problems with a medium size domain space. Profiling has shown a large portion of this may be attributable to the performance of Swift's native Dictionary type. Improved heuristics such as MAC3 are planned (spaces in the source code are left for them and contributions are welcome!) and should improve the situation. You can turn on the MRV or LCV heuristics (which are already implemented) when calling `backtrackingSearch()` to improve performance in many instances. In my testing MRV improves many searches, whereas the LCV implementation still leaves something to be desired, but may be useful in very specific problems. Note that these heuristics can also *decrease* performance for some problems.
5758

5859
## Generics
5960
SwiftCSP makes extensive use of generics. It seems like a lot of unnecessary angle brackets, but it allows the type checker to ensure variables fit with their domains and constraints. Due to a limitation in Swift generics, `Constraint` is a class instead of a protocol.
6061

6162
## Help Wanted
62-
Contributions that implement heuristics, improve performance in other ways, or simplify the design are more than welcome. Just make sure all of the unit tests still run and the new version maintains the flexibility of having any `Hashable` type as a variable and any type as a `Domain`. Additional unit tests are also welcome.
63+
Contributions that implement heuristics, improve performance in other ways, or simplify the design are more than welcome. Just make sure all of the unit tests still run and the new version maintains the flexibility of having any `Hashable` type as a variable and any type as a `Domain`. Additional unit tests are also welcome. A simple MinConflicts solver is also implemented, but could be improved.
6364

6465
## Authorship and License
6566
SwiftCSP was written by David Kopec and released under the Apache License (see `LICENSE`). It was originally a port of a Dart library I wrote called [constraineD](https://github.com/davecom/constraineD) which itself was a port of a Python library I wrote many years before that.

Sources/SwiftCSP/Backtrack.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
/// - parameter csp: The CSP to operate on.
2222
/// - parameter assignment: Optionally, an already partially completed assignment.
2323
/// - parameter mrv: Should it use the mrv heuristic to try to improve performance (default false)
24-
/// - parameter lcv: SHould it use the lcv heuristic to try to improve performance (default false) NOT IMPLEMENTED YET
24+
/// - parameter lcv: SHould it use the lcv heuristic to try to improve performance (default false)
2525
/// - parameter mac3: SHould it use the mac3 heuristic to try to improve performance (default false) NOT IMPLEMENTED YET
2626
/// - returns: the assignment (solution), or nil if none can be found
2727
public func backtrackingSearch<V, D>(csp: CSP<V, D>, assignment: Dictionary<V, D> = Dictionary<V, D>(), mrv: Bool = false, lcv: Bool = false, mac3: Bool = false) -> Dictionary<V, D>?
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//
2+
// MinConflicts.swift
3+
// SwiftCSP
4+
//
5+
// Copyright (c) 2022 David Kopec
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
19+
/// Use the MinConflicts algorithm to try to resolve the CSP
20+
///
21+
/// - parameter csp: The CSP to operate on.
22+
/// - parameter maxSteps: Maxmimum number of steps to try
23+
/// - parameter assignment: Optionally, an already full assignment, .
24+
/// - returns: the assignment (solution), and any remaining conflicted variables
25+
public func minConflicts<V, D>(csp: CSP<V, D>, maxSteps: Int = 100000, assignment: Dictionary<V, D>?) -> (Dictionary<V, D>, Array<V>)
26+
{
27+
var current = (assignment != nil) ? assignment! : randomAssignment(csp: csp)
28+
for _ in 0..<maxSteps {
29+
let conflicted = conflictedVariables(csp: csp, assignment: current)
30+
if conflicted.isEmpty { return (current, []) }
31+
let variable = conflicted.randomElement()!
32+
let value = minimallyConflictedValue(csp: csp, variable: variable, assignment: current)
33+
current[variable] = value
34+
}
35+
return (current, conflictedVariables(csp: csp, assignment: current))
36+
}
37+
38+
/// First uses the MinConflicts algorithm to try to resolve the CSP.
39+
/// Then, if it's not solved after *maxSteps*, runs a backtracking search to find
40+
/// domain values for the remaining conflicted variables.
41+
///
42+
/// - parameter csp: The CSP to operate on.
43+
/// - parameter maxSteps: Maxmimum number of steps to try for the MinConflicts portion
44+
/// - parameter assignment: Optionally, an already full assignment.
45+
/// - parameter mrv: Should it use the mrv heuristic to try to improve performance (default false)
46+
/// - parameter lcv: SHould it use the lcv heuristic to try to improve performance (default false)
47+
/// - parameter mac3: SHould it use the mac3 heuristic to try to improve performance (default false) NOT IMPLEMENTED YET
48+
/// - returns: the assignment (solution), and any remaining conflicted variables
49+
//public func hybridSearch<V, D>(csp: CSP<V, D>, maxSteps: Int = 100000, assignment: Dictionary<V, D>?, mrv: Bool = false, lcv: Bool = false, mac3: Bool = false) -> Dictionary<V, D>? {
50+
// let (current, conflicted) = minConflicts(csp: csp, maxSteps: maxSteps, assignment: assignment)
51+
// if conflicted.isEmpty {
52+
// return current
53+
// }
54+
// // Incomplete assignment, so remove conflicting variables from it
55+
// let partial = current.filter { !conflicted.contains($0.0) }
56+
// return backtrackingSearch(csp: csp, assignment: partial, mrv: mrv, lcv: lcv, mac3: mac3)
57+
//}
58+
59+
public func minimallyConflictedValue<V, D>(csp: CSP<V, D>, variable: V, assignment: Dictionary<V, D>) -> D {
60+
let domain = csp.domains[variable]!
61+
var minimallyConstrained = domain[0]
62+
var minimumConflictCount = countConflicts(csp: csp, variable: variable, value: minimallyConstrained, assignment: assignment)
63+
for domainValue in domain.dropFirst() {
64+
let conflictCount = countConflicts(csp: csp, variable: variable, value: domainValue, assignment: assignment)
65+
if conflictCount < minimumConflictCount {
66+
minimumConflictCount = conflictCount
67+
minimallyConstrained = domainValue
68+
}
69+
}
70+
return minimallyConstrained
71+
}
72+
73+
public func countConflicts<V, D>(csp: CSP<V, D>, variable: V, value: D, assignment: Dictionary<V, D>) -> Int {
74+
var current = assignment
75+
current[variable] = value
76+
var count = 0
77+
for constraint in csp.constraints[variable]! {
78+
if !constraint.isSatisfied(assignment: current) {
79+
count += 1
80+
}
81+
}
82+
return count
83+
}
84+
85+
public func conflictedVariables<V, D>(csp: CSP<V, D>, assignment: Dictionary<V, D>) -> [V] {
86+
var conflicted: [V] = []
87+
outer: for variable in csp.variables {
88+
for constraint in csp.constraints[variable]! {
89+
if !constraint.isSatisfied(assignment: assignment) {
90+
conflicted.append(variable)
91+
continue outer
92+
}
93+
}
94+
}
95+
return conflicted
96+
}
97+
98+
public func randomAssignment<V, D>(csp: CSP<V, D>) -> Dictionary<V, D>
99+
{
100+
var assignment = Dictionary<V, D>()
101+
for variable in csp.variables {
102+
assignment[variable] = csp.domains[variable]!.randomElement()
103+
}
104+
return assignment
105+
}

SwiftCSP.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
Pod::Spec.new do |s|
22
s.name = 'SwiftCSP'
3-
s.version = '0.9.8'
3+
s.version = '0.9.9'
44
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
55
s.summary = 'A Constraint Satisfaction Problem Solver in Pure Swift'
66
s.homepage = 'https://github.com/davecom/SwiftCSP'
77
s.social_media_url = 'https://twitter.com/davekopec'
88
s.authors = { 'David Kopec' => 'david@oaksnow.com' }
99
s.source = { :git => 'https://github.com/davecom/SwiftCSP.git', :tag => s.version }
10-
s.swift_versions = ['5.0', '5.1']
10+
s.swift_versions = ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6']
1111
s.ios.deployment_target = '8.0'
1212
s.osx.deployment_target = '10.9'
1313
s.tvos.deployment_target = '10.0'

SwiftCSP.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
550E6B1D1B604E9600CDE757 /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550E6B1C1B604E9600CDE757 /* Constraint.swift */; };
1818
550E6B1E1B604E9600CDE757 /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550E6B1C1B604E9600CDE757 /* Constraint.swift */; };
1919
550E6B1F1B604E9A00CDE757 /* CSP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550E6B1A1B604CC400CDE757 /* CSP.swift */; };
20+
551266B828B555CE00F13BFC /* MinConflicts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551266B728B555CE00F13BFC /* MinConflicts.swift */; };
21+
551266B928B555CE00F13BFC /* MinConflicts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551266B728B555CE00F13BFC /* MinConflicts.swift */; };
22+
551266BB28B9515800F13BFC /* SudokuTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551266BA28B9515800F13BFC /* SudokuTest.swift */; };
2023
55CD29161B63189800DF47C5 /* AustralianMapColoringTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CD29151B63189800DF47C5 /* AustralianMapColoringTest.swift */; };
2124
55CD29181B6327CA00DF47C5 /* EightQueensTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CD29171B6327CA00DF47C5 /* EightQueensTest.swift */; };
2225
55CD291A1B63339B00DF47C5 /* CircuitBoard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CD29191B63339B00DF47C5 /* CircuitBoard.swift */; };
@@ -46,6 +49,8 @@
4649
550E6B0F1B604AF500CDE757 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = SOURCE_ROOT; };
4750
550E6B1A1B604CC400CDE757 /* CSP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CSP.swift; path = Sources/SwiftCSP/CSP.swift; sourceTree = "<group>"; };
4851
550E6B1C1B604E9600CDE757 /* Constraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Constraint.swift; path = Sources/SwiftCSP/Constraint.swift; sourceTree = "<group>"; };
52+
551266B728B555CE00F13BFC /* MinConflicts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MinConflicts.swift; path = Sources/SwiftCSP/MinConflicts.swift; sourceTree = "<group>"; };
53+
551266BA28B9515800F13BFC /* SudokuTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SudokuTest.swift; path = Tests/SwiftCSPTests/SudokuTest.swift; sourceTree = SOURCE_ROOT; };
4954
55CD29151B63189800DF47C5 /* AustralianMapColoringTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AustralianMapColoringTest.swift; path = Tests/SwiftCSPTests/AustralianMapColoringTest.swift; sourceTree = SOURCE_ROOT; };
5055
55CD29171B6327CA00DF47C5 /* EightQueensTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EightQueensTest.swift; path = Tests/SwiftCSPTests/EightQueensTest.swift; sourceTree = SOURCE_ROOT; };
5156
55CD29191B63339B00DF47C5 /* CircuitBoard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CircuitBoard.swift; path = Demo/CircuitBoard.swift; sourceTree = SOURCE_ROOT; };
@@ -77,6 +82,7 @@
7782
550E6B1A1B604CC400CDE757 /* CSP.swift */,
7883
550E6B1C1B604E9600CDE757 /* Constraint.swift */,
7984
55047EAB1B6158BB00309A30 /* Backtrack.swift */,
85+
551266B728B555CE00F13BFC /* MinConflicts.swift */,
8086
550E6AFC1B604AF500CDE757 /* SwiftCSP */,
8187
550E6B0D1B604AF500CDE757 /* SwiftCSPTests */,
8288
550E6AFB1B604AF500CDE757 /* Products */,
@@ -119,6 +125,7 @@
119125
children = (
120126
55047EAE1B616DE000309A30 /* SendMoreMoneyTest.swift */,
121127
55CD29151B63189800DF47C5 /* AustralianMapColoringTest.swift */,
128+
551266BA28B9515800F13BFC /* SudokuTest.swift */,
122129
55CD29171B6327CA00DF47C5 /* EightQueensTest.swift */,
123130
550E6B0E1B604AF500CDE757 /* Supporting Files */,
124131
);
@@ -242,6 +249,7 @@
242249
55CD291C1B6339BE00DF47C5 /* LayoutView.swift in Sources */,
243250
55CD291A1B63339B00DF47C5 /* CircuitBoard.swift in Sources */,
244251
550E6B1B1B604CC400CDE757 /* CSP.swift in Sources */,
252+
551266B828B555CE00F13BFC /* MinConflicts.swift in Sources */,
245253
550E6B001B604AF500CDE757 /* AppDelegate.swift in Sources */,
246254
);
247255
runOnlyForDeploymentPostprocessing = 0;
@@ -253,8 +261,10 @@
253261
55CD29181B6327CA00DF47C5 /* EightQueensTest.swift in Sources */,
254262
55047EAD1B6158BB00309A30 /* Backtrack.swift in Sources */,
255263
55CD29161B63189800DF47C5 /* AustralianMapColoringTest.swift in Sources */,
264+
551266B928B555CE00F13BFC /* MinConflicts.swift in Sources */,
256265
55047EB01B616DE000309A30 /* SendMoreMoneyTest.swift in Sources */,
257266
550E6B1E1B604E9600CDE757 /* Constraint.swift in Sources */,
267+
551266BB28B9515800F13BFC /* SudokuTest.swift in Sources */,
258268
550E6B1F1B604E9A00CDE757 /* CSP.swift in Sources */,
259269
);
260270
runOnlyForDeploymentPostprocessing = 0;

Tests/LinuxMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ XCTMain([
55
testCase(AustralianMapColoringTest.allTests),
66
testCase(EightQueensTest.allTests),
77
testCase(SendMoreMoneyTest.allTests),
8+
testCase(SudokuTest.allTests),
89
])

0 commit comments

Comments
 (0)