├── project.clj ├── CHANGES.md ├── LICENSE ├── README.md └── src └── rolling_stones └── core.clj /project.clj: -------------------------------------------------------------------------------- 1 | (defproject rolling-stones "1.0.3" 2 | :description "rolling-stones is a Clojure interface to the Sat4j satisfaction solver" 3 | :url "https://github.com/Engelberg/rolling-stones" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.10.3"] 7 | [better-cond "2.1.0"] 8 | [org.ow2.sat4j/org.ow2.sat4j.core "2.3.6"]]) 9 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Rolling Stones Change Log 2 | 3 | ## 1.0.3 4 | 5 | * Added exclusion for abs to prevent warning on new version of Clojure which has abs in core. 6 | 7 | ## 1.0.2 8 | 9 | * Updated to use version 2.3.6 of SAT4J 10 | * Fixed memory leak that occurred when no timeout is set 11 | * Added note to README that when timeout is set (in milliseconds), solver can't be garbage collected until timeout is reached. 12 | * Added `*timeout-on-conflicts*` dynamic variable to control whether timeout integer is interpreted as number of conflicts instead of milliseconds. 13 | 14 | ## 1.0.1 15 | 16 | ### Bugfixes 17 | 18 | * Fixed bug in `and?` 19 | 20 | ## 1.0.0 - Initial Release 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rolling Stones 2 | 3 | *"I can't get no..."* 4 | 5 | Rolling Stones is a Clojure interface to the Sat4j satisfaction solver. 6 | 7 | A satisfaction solver uses a combination of brute force searching and clever heuristics to determine whether a given Boolean formula is satisfiable, that is, whether an assignment of truth values for the Boolean variables exists that causes the entire formula to be true. 8 | 9 | Satisfiability (aka SAT) was the first computer science problem to be proven NP-complete. Most other NP-complete problems are proven NP-complete by showing that a reduction to SAT exists. This makes a SAT solver an especially powerful tool, because many interesting problems (NP-complete and otherwise) can be encoded as a SAT problem. SAT solving heuristics are an area of intensive study in academic research, and recent advancements in SAT solvers allow us to crack many classes of moderately complex SAT problems in short order. 10 | 11 | ## Usage 12 | 13 | Rolling Stones currently requires Clojure 1.9 alpha 14 or higher. 14 | 15 | ``` 16 | [rolling-stones "1.0.3"] 17 | 18 | (require '[rolling-stones.core :as sat :refer [! at-least at-most exactly]]) 19 | ``` 20 | 21 | ### Conjunctive Normal Form 22 | 23 | The most efficient way to invoke the SAT solver is to hand it a problem that is already in *conjunctive normal form*, i.e., it is an AND of a bunch ORs. Each OR is comprised of only plain variables and the NOT of plain variables. 24 | 25 | Example of Conjunctive Normal Form (written in infix notation): 26 | 27 | p AND (p OR q) AND ((NOT p) OR q OR (NOT r)) 28 | 29 | SAT4j expects us to use integers (starting from 1) for our variables, and negative integers represent the NOT of a variable. If we let 1 stand for p, 2 stand for q, and 3 stand for r, the above formula would be expressed in Clojure as: 30 | 31 | `[[1] [1 2] [-1 2 -3]]` 32 | 33 | In other words, we create a vector of vectors. Each inner vector represents an OR clause, and the outer vector represents the overall AND. 34 | 35 | We can solve this as follows: 36 | 37 | ```clojure 38 | => (sat/solve [[1] [1 2] [-1 2 -3]]) 39 | [1 -2 -3] 40 | ``` 41 | 42 | This answer tells us that if variable 1 (p) is set to true and variables 2 and 3 (q and r) are set to false, the overall formula will be true. 43 | A variety of statistics about the solving process are attached as metadata to the solution: 44 | 45 | ```clojure 46 | => (meta (sat/solve [[1] [1 2] [-1 2 -3]])) 47 | {:shortcuts 0, 48 | :decisions 2, 49 | :reduced-db 0, 50 | :learned-ternary-clauses 0, 51 | :root-simplifications 0, 52 | :conflicts 0, 53 | :update-lbd 0, 54 | :reduced-literals 0, 55 | :ignored-clauses 0, 56 | :imported-units 0, 57 | :starts 1, 58 | :changed-reason 0, 59 | :learned-binary-clauses 0, 60 | :propagations 3, 61 | :inspects 1, 62 | :learned-clauses 0, 63 | :learned-literals 0} 64 | ``` 65 | 66 | 67 | We can get all the true combinations as follows: 68 | 69 | ```clojure 70 | => (sat/solutions [[1] [1 2] [-1 2 -3]]) 71 | ([1 -2 -3] [1 2 -3] [1 2 3]) 72 | ``` 73 | 74 | Both functions take an optional second argument, the timeout in milliseconds. If you need to know when and whether a timeout has occurred, you can pass in an optional third argument, an atom which will be set to true upon timeout. 75 | 76 | Keep in mind that `solutions` produces a lazy sequence, so if you specify a timeout, you need to do something to force the evaluation of the lazy sequence before the timeout occurs. 77 | 78 | Another point to be aware of relating to timeouts: due to the architecture of SAT4J, if you specify a timeout, the solver is not eligible for garbage collection until the timeout time has elapsed, because SAT4J creates a separate timer process that holds a reference to the solver. This could be an issue if you solve a lot of small problems rapidly, each with a long timeout. If this poses a problem for you, you can bind the dynamic variable `*timeout-on-conflicts*` to true when invoking your solver, which causes the timeout integer to be interpreted as a limit on the number of conflicts encountered in the solving process, rather than a number of milliseconds -- conflict-based timeouts do not have this GC caveat. Conflict-based timeouts are also useful in scenarios that require reproducible results across different platforms. 79 | 80 | Unsatisfiable formulas return nil: 81 | 82 | ```clojure 83 | => (solve [[1] [-1]]) 84 | nil 85 | ``` 86 | 87 | The underlying SAT solver, Sat4j, is especially interesting because it supports additional *constraints* in which you specify that at least, at most, or exactly some number of a set of variables must be true. 88 | 89 | Examples of constraints: 90 | 91 | `(exactly 2 [1 2 3])` - exactly 2 of the variables 1, 2, 3 must be true 92 | 93 | `(at-least 3 [1 -2 3 -4 5])` - at least 3 of 1, -2, 3, -4, 5 must be true 94 | 95 | `(at-most 1 [-1 -2 -3])` - at most 1 of -1, -2, -3 must be true 96 | 97 | These constraints can be included inside the outer vector. 98 | 99 | ```clojure 100 | => (sat/solutions [[-1 2 -3] [1 2 3 4 5] (at-least 3 [2 3 4 5])]) 101 | ([-1 2 3 4 -5] [-1 2 3 -4 5] [-1 2 -3 4 5] [-1 -2 3 4 5] [-1 2 3 4 5] [1 2 3 -4 5] [1 2 3 4 -5] [1 2 -3 4 5] [1 2 3 4 5]) 102 | ``` 103 | 104 | ### Symbolic Conjunctive Normal Form 105 | 106 | The underlying Java solver wants the formula in terms of numbers, but in Clojure we're used to working at a higher-level and we want to be able to express variables with arbitrary Clojure data structures, maybe symbols, keywords, numbers, vectors, records, etc. 107 | 108 | Rolling Stones lets you use anything you want as variables by calling the more versatile functions `solve-symbolic-cnf` or `solutions-symbolic-cnf`. Again our input is a vector of vectors, with the inner vectors representing OR, and the outer vector representing AND. Not is expressed by calling the function `!`. 109 | 110 | For the purposes of our example, let's use keywords for our variables (although any valid Clojure data would work). 111 | 112 | ```clojure 113 | => (sat/solve-symbolic-cnf [[:p] [:p :q] [(! :p) :q (! :r)]]) 114 | [:p (! :q) (! :r)] 115 | => (sat/solutions-symbolic-cnf [[:p] [:p :q] [(! :p) :q (! :r)]]) 116 | ([:p (! :q) (! :r)] [:p :q (! :r)] [:p :q :r]) 117 | => (sst/solutions-symbolic-cnf [[:p] [:p :q] [(! :p) :q (! :r)] 118 | (at-least 2 [:p :q :r])]) 119 | ([:p :q (! :r)] [:p :q :r]) 120 | ``` 121 | 122 | But remember, you can use any Clojure data you want to denote variables. You don't need to restrict yourself to keywords. As a somewhat crazy example, let's replace `:p` with `{:name "Bob"}`, `:q` with `[2 6]` and `:r` with `#{4}`. 123 | 124 | ```clojure 125 | => (sat/solve-symbolic-cnf [[{:name "Bob"}] [{:name "Bob"} [2 6]] 126 | [(! {:name "Bob"}) [2 6] (! #{4})]]) 127 | [[{:name "Bob"}] [{:name "Bob"} [2 6]] [(! {:name "Bob"}) [2 6] (! #{4})]] 128 | ``` 129 | 130 | Behind the scenes, Rolling Stones is simply translating each distinct Clojure value to a distinct integer (negative integer if wrapped in a `!`) and passing it to the integer variable solver discussed above, then translating the output back into the Clojure values you used. 131 | 132 | ### Symbolic formulas 133 | 134 | If you don't have your logical formula in conjunctive normal form, you can build it using the functions AND, OR, NOT (which is just an alias for !), XOR, IFF (if and only if, aka biconditional), IMP (for implies, aka conditional, e.g., P -> Q), NOR, and NAND -- Rolling Stones will convert the formula to CNF for you. These are Clojure functions, so we express our formulas with prefix notation. AND, OR, NOR, and NAND take any number of inputs, NOT of course takes only one input, and the rest take exactly two inputs. 135 | 136 | The functions `solve-symbolic-formula` and `solutions-symbolic-formula` can either take a single formula: 137 | 138 | ```clojure 139 | (require '[rolling-stones.core :as sat :refer [! NOT AND OR XOR IFF IMP NOR NAND at-least at-most exactly]]) 140 | 141 | => (sat/solve-symbolic-formula (XOR (AND :p :q (! :r)) (IFF :p (IMP :q :r)))) 142 | [:q (! :p) (! :r)] 143 | => (sat/solutions-symbolic-formula (XOR (AND :p :q (! :r)) (IFF :p (IMP :q :r)))) 144 | ([:q (! :p) (! :r)] [:q :p (! :r)] [(! :q) :p (! :r)] [(! :q) :p :r] [:q :p :r]) 145 | ``` 146 | 147 | or a sequence of formulas and/or constraints which are implicitly joined with AND: 148 | 149 | ```clojure 150 | => (sat/solve-symbolic-formula [(XOR :p :q) (NAND :q :r) (at-most 1 [:p :q :r])]) 151 | [:p (! :q) (! :r)] 152 | ``` 153 | 154 | Behind the scenes, Rolling Stones is expanding the formula into conjunctive normal form by way of something called Tseitin encoding, which introduces a temporary variable for each sub-expression in the formula. It solves the expression using the above solver, and then reports back the values of all the original variables (ignoring the temporary variables it created). 155 | 156 | As you can see, the combination of the speedy underlying Sat4j Java-based solver and the high-level ease of manipulating symbolic data via Clojure makes for a very powerful combination. 157 | 158 | ### Working with the output 159 | 160 | Now that you've solved your logical formula, what do you do with the output? There are two types of formulas supported by Rolling Stones -- those where the variables are represented by integers, and those where the variables are represented symbolically by arbitrary data. Let's look at each of these formula types in turn. 161 | 162 | #### Integer variable solutions 163 | 164 | The output from `solve`, the solver for integer variable CNF inputs, will be a vector of numbers, positive for true and negative for false, e.g., `[1 -2 -3]`. 165 | 166 | So Clojure's `pos?` predicate is our tool for testing for a true variable, `neg?` is our tool for testing for a false variable, and `-` is the way we negate variables, changing true to false and false to true. This gives us everything we need to manipulate the result. 167 | 168 | Examples: 169 | 170 | ```clojure 171 | ; Get all true variables 172 | => (filter pos? [1 -2 -3]) 173 | 174 | ; Get all false variables 175 | => (filter neg? [1 -2 -3]) 176 | 177 | ; Get all false variables, drop the minus sign 178 | => (map - (filter neg? [1 -2 -3])) 179 | 180 | ; Swap true and false variables 181 | => (mapv - [1 -2 -3]) 182 | ``` 183 | 184 | I find that 90% of the time, I want to build up a set of all the true variables so I can query against it. So I've added a convenience function to do just that, namely `true-integer-variables`. 185 | 186 | ```clojure 187 | ; Collect all the true variables into a set 188 | (def true-set (true-integer-variables [1 -2 -3])) 189 | 190 | ; Test if a specific variable is true 191 | => (contains? true-set 1) 192 | true 193 | => (contains? true-set 2) 194 | false 195 | ``` 196 | 197 | Of course, you can accomplish the same thing nearly as concisely with Clojure's built-in functions: 198 | 199 | ```clojure 200 | (def true-set (into #{} (filter pos?) [1 -2 -3])) 201 | ``` 202 | 203 | #### Symbolic variable solutions 204 | 205 | When you use one of the symbolic solves, you get back a result like `[:p (! :q) (! :r)]`. What exactly does this mean? Specifically, how is a not like `(! q)` represented? 206 | 207 | Internally, `!` is just a constructor that builds a Not record, and I've tweaked the way it prints. If the printing weren't tweaked, the output would look like this: 208 | 209 | ```clojure 210 | => (! :x) 211 | #rolling_stones.core.Not{:literal :x} 212 | ``` 213 | 214 | So as you can see, the contents of the Not are stored in a field called `:literal`, so if you need to extract the contents out of a Not, that's one way to do it (we'll see another way in a moment that doesn't require knowing the internal representation of Not). 215 | 216 | It is convenient to extend the metaphor of integer variables to symbolic variables, and we can think of true variables as "positive" and false variables as "negative". So, for symbolic variables, Rolling Stones provides the predicates `positive?` and `negative?` to test symbolic variables, and `negate` which swaps positive with negative and vice versa. 217 | 218 | Examples: 219 | 220 | ```clojure 221 | => (sat/positive? :x) 222 | true 223 | => (sat/negative? :x) 224 | false 225 | => (sat/positive? (! :x)) 226 | false 227 | => (sat/negative? (! :x)) 228 | true 229 | => (sat/negate :x) 230 | (! :x) 231 | => (sat/negate (! :x)) 232 | :x 233 | 234 | ; ! just builds a Not record, so doesn't have a notion of double negative 235 | => (! (! :x)) 236 | (! (! :x)) 237 | 238 | ; But negate does 239 | => (sat/negate (sat/negate :x)) 240 | :x 241 | ``` 242 | 243 | As demonstrated above, negate is another way to extract the contents of a Not, without needing to know the internal representation of a Not. 244 | 245 | With these tools in place, you can do exactly the same sorts of manipulations that we did with integer variable solutions: 246 | 247 | ```clojure 248 | ; Get all true variables 249 | => (filter sat/positive? [:p (! :q) (! :r)]) 250 | 251 | ; Get all false variables 252 | => (filter sat/negative? [:p (! :q) (! :r)]) 253 | 254 | ; Get all false variables, drop the minus sign 255 | => (map sat/negate (filter sat/negative? [:p (! :q) (! :r)])) 256 | 257 | ; Swap true and false variables 258 | => (mapv sat/negate [:p (! :q) (! :r)]) 259 | ``` 260 | 261 | Again, there is a convenience function to collect the true symbolic variables into a set. 262 | 263 | ```clojure 264 | ; Collect all the true variables into a set 265 | (def true-set (true-symbolic-variables [:p (! :q) (! :r)])) 266 | 267 | ; or you can use Clojure's functions to accomplish the same thing 268 | (def true-set (into #{} (filter sat/positive?) [:p (! :q) (! :r)])) 269 | 270 | ; Test if a specific variable is true 271 | => (contains? true-set :p) 272 | true 273 | => (contains? true-set :q) 274 | false 275 | ``` 276 | 277 | ## API Summary 278 | 279 | * Integer Variables, Conjunctive Normal Form 280 | + `solve` 281 | + `solutions` 282 | * Symbolic Variables, Conjunctive Normal Form 283 | + `solve-symbolic-cnf` 284 | + `solutions-symbolic-cnf` 285 | + `!` to represent the Not of a variable 286 | * Symbolic Variables, any logic formula 287 | + `solve-symbolic-formula` 288 | + `solutions-symbolic-formula` 289 | + `NOT` (same as `!`), `AND`, `OR`, `XOR`, `IFF`, `IMP`, `NAND`, `NOR` to build formula 290 | * Manipulating integer solutions 291 | + Use clojure.core's `pos?`, `neg?`, and `-` 292 | + `true-integer-variables` builds a set of the true integer variables, i.e., the positive integers 293 | * Manipulating symbolic solutions 294 | + Use rolling-stones.core's `positive?`, `negative?`, and `negate` 295 | + `true-symbolic-variables` builds a set of the true symbolic variables, i.e., any Clojure data that isn't a Not. 296 | * Retrieving statistics about the solving process 297 | + Call clojure.core's `meta` on any solution 298 | * Return value of nil indicates no solution 299 | 300 | ## Roadmap 301 | 302 | As far as my own personal needs go, this project is feature complete. However, Sat4j has a ton of extra functionality that isn't exposed here, for example, it can solve MAXSAT, Pseudo-Boolean problems, and MUS problems, and there are a bunch of alternative solving algorithms as well as tools that can read formulas out of files in a variety of standardized formats. I would welcome pull requests that expose more aspects of the underlying solver in a similarly Clojure-friendly way. 303 | 304 | ## License 305 | 306 | Copyright © 2016 Mark Engelberg 307 | 308 | Distributed under the Eclipse Public License either version 1.0 or (at 309 | your option) any later version. 310 | 311 | -------------------------------------------------------------------------------- /src/rolling_stones/core.clj: -------------------------------------------------------------------------------- 1 | (ns rolling-stones.core 2 | (:refer-clojure :exclude [abs]) 3 | (:require [better-cond.core :as b] 4 | [clojure.spec.alpha :as s] 5 | [clojure.pprint]) 6 | (:import [org.sat4j.core VecInt] 7 | org.sat4j.minisat.SolverFactory 8 | org.sat4j.minisat.core.SolverStats 9 | org.sat4j.tools.ModelIterator 10 | [org.sat4j.specs ContradictionException TimeoutException ISolver IProblem])) 11 | 12 | (def ^:dynamic *timeout-on-conflicts* false) 13 | 14 | (defn- abs [x] (if (neg? x) (- x) x)) 15 | 16 | (defn- vec-int [v] 17 | (VecInt. (int-array v))) 18 | 19 | (defrecord Constraint [type n literals]) 20 | (defn at-most [n literals] (->Constraint :at-most n literals)) 21 | (defn at-least [n literals] (->Constraint :at-least n literals)) 22 | (defn exactly [n literals] (->Constraint :exactly n literals)) 23 | (defn constraint? [c] (instance? Constraint c)) 24 | 25 | (defn- add-at-most [^ISolver solver literals n] 26 | (.addAtMost solver (vec-int literals) n)) 27 | 28 | (defn- add-at-least [^ISolver solver literals n] 29 | (.addAtLeast solver (vec-int literals) n)) 30 | 31 | (defn- add-exactly [^ISolver solver literals n] 32 | (.addExactly solver (vec-int literals) n)) 33 | 34 | (defn- add-constraint [solver constraint] 35 | ((case (:type constraint) 36 | :at-most add-at-most 37 | :at-least add-at-least 38 | :exactly add-exactly) 39 | solver (:literals constraint) (:n constraint))) 40 | 41 | (defn- create-solver ^ISolver [clauses] 42 | (let [^ISolver solver (SolverFactory/newDefault), 43 | {constraints true, clauses false} (group-by constraint? clauses) 44 | max-var (max (transduce (comp cat (map abs)) max 1 clauses) 45 | (transduce (comp (map :literals) cat (map abs)) max 1 constraints))] 46 | (.newVar solver max-var) 47 | (.setExpectedNumberOfClauses solver (count clauses)) 48 | (try 49 | (dotimes [i (count clauses)] 50 | (.addClause solver (vec-int (clauses i)))) 51 | (dotimes [i (count constraints)] 52 | (add-constraint solver (constraints i))) 53 | solver 54 | (catch ContradictionException e nil)))) 55 | 56 | (defn- set-timeout-atom! [timeout-atom] 57 | (when timeout-atom (do (reset! timeout-atom true) nil))) 58 | 59 | (defn- find-model [^ISolver solver timeout-atom] 60 | (try 61 | (.findModel solver) 62 | (catch TimeoutException e (set-timeout-atom! timeout-atom)))) 63 | 64 | (defn- stats-map [^ISolver solver] 65 | (let [^SolverStats stats (.getStats solver)] 66 | {:starts (.getStarts stats), 67 | :decisions (.getDecisions stats), 68 | :propagations (.getPropagations stats), 69 | :inspects (.getInspects stats), 70 | :shortcuts (.getShortcuts stats), 71 | :conflicts (.getConflicts stats), 72 | :learned-literals (.getLearnedliterals stats), 73 | :learned-clauses (.getLearnedclauses stats), 74 | :ignored-clauses (.getIgnoredclauses stats), 75 | :learned-binary-clauses (.getLearnedbinaryclauses stats), 76 | :learned-ternary-clauses (.getLearnedternaryclauses stats), 77 | :root-simplifications (.getRootSimplifications stats), 78 | :reduced-literals (.getReducedliterals stats), 79 | :changed-reason (.getChangedreason stats), 80 | :reduced-db (.getReduceddb stats), 81 | :update-lbd (.getUpdateLBD stats), 82 | :imported-units (.getImportedUnits stats)})) 83 | 84 | 85 | (s/def ::constraint constraint?) 86 | (s/def ::numeric-clause (s/coll-of (s/and int? #(not= % 0)) 87 | :into [])) 88 | (s/fdef solve 89 | :args (s/cat :clauses (s/coll-of (s/or :constraint ::constraint 90 | :clause ::numeric-clause) 91 | :into ()) 92 | :timeout (s/? pos-int?)) 93 | :ret (s/nilable ::numeric-clause)) 94 | 95 | (defn solve 96 | "Takes a formula in integer CNF form, i.e., vector of constraints and vectors of non-zero ints 97 | and optional timeout in millis and optional atom to set to true if timeout occurs. 98 | Returns solution or nil if no solution exists. Stats attached as metadata." 99 | ([clauses] (solve clauses nil nil)) 100 | ([clauses timeout] (solve clauses timeout nil)) 101 | ([clauses timeout timeout-atom] 102 | (when-let [solver (create-solver clauses)] 103 | (if timeout 104 | (if *timeout-on-conflicts* 105 | (.setTimeoutOnConflicts solver timeout) 106 | (.setTimeoutMs solver timeout)) 107 | (.setTimeoutOnConflicts solver Integer/MAX_VALUE)) 108 | (when-let [solution (find-model solver timeout-atom)] 109 | (with-meta (vec solution) (stats-map solver)))))) 110 | 111 | (defn- find-next-model [^ISolver solver timeout-atom] 112 | (try 113 | (when (.isSatisfiable solver) 114 | (.model solver)) 115 | (catch TimeoutException e (set-timeout-atom! timeout-atom)))) 116 | 117 | (s/fdef solutions 118 | :args (s/cat :clauses (s/coll-of (s/or :constraint ::constraint 119 | :clause ::numeric-clause) 120 | :into ()) 121 | :timeout (s/? pos-int?)) 122 | :ret (s/* ::numeric-clause)) 123 | 124 | (defn solutions 125 | "Takes a formula in integer CNF form, i.e., vector of constraints and vectors of non-zero ints 126 | and optional timeout in millis and optional atom to set to true if timeout occurs. 127 | Returns lazy sequence of all solutions. Stats attached as metadata to each solution." 128 | ([clauses] (solutions clauses nil nil)) 129 | ([clauses timeout] (solutions clauses timeout nil)) 130 | ([clauses timeout timeout-atom] 131 | (b/cond 132 | :when-let [solver (create-solver clauses)] 133 | :let [iterator (ModelIterator. solver) 134 | _ (if timeout 135 | (if *timeout-on-conflicts* 136 | (.setTimeoutOnConflicts solver timeout) 137 | (.setTimeoutMs solver timeout)) 138 | (.setTimeoutOnConflicts solver Integer/MAX_VALUE)) 139 | solution-iterator (fn [_] 140 | (when-let [next-solution (find-next-model iterator timeout-atom)] 141 | (with-meta (vec next-solution) (stats-map solver))))] 142 | :when-let [first-solution (solution-iterator nil)] 143 | (take-while identity (iterate solution-iterator first-solution))))) 144 | 145 | (defn- make-counter "Makes counter starting from n" [n] 146 | (let [ctr (atom (dec n))] 147 | (memoize (fn [x] (swap! ctr inc))))) 148 | 149 | (defrecord Not [literal]) 150 | (def ! ->Not) 151 | (def NOT ->Not) 152 | (defn not? [r] (instance? Not r)) 153 | 154 | (defmethod clojure.core/print-method Not [x writer] 155 | (binding [*out* writer] 156 | (print "(! ") 157 | (pr (:literal x)) 158 | (print ")"))) 159 | 160 | (. clojure.pprint/simple-dispatch addMethod Not #(clojure.core/print-method % *out*)) 161 | 162 | (defn- make-id-generator [] 163 | (let [id (make-counter 1)] 164 | (fn [x] 165 | (cond 166 | (instance? Not x) (- (id (:literal x))) 167 | :else (id x))))) 168 | 169 | (defn- all-literals [clauses] 170 | (into [] (comp 171 | (mapcat (fn [clause] (if (constraint? clause) (:literals clause) clause))) 172 | (map (fn [l] (if (not? l) (:literal l) l))) 173 | (distinct)) 174 | clauses)) 175 | 176 | (defn- build-transforms [clauses] 177 | (let [literals (all-literals clauses) 178 | object->int (into {} (for [i (range (count literals))] 179 | [(literals i) (inc i)])), 180 | int->object (fn [i] (literals (dec i)))] 181 | [(fn [o] (if (not? o) (- (object->int (:literal o))) (object->int o))) 182 | (fn [i] (if (neg? i) (! (int->object (- i))) (int->object i)))])) 183 | 184 | (defn- clause-transformer [transform] 185 | (fn [clause] 186 | (if (constraint? clause) 187 | (update clause :literals (partial mapv transform)) 188 | (if-let [m (meta clause)] 189 | (with-meta (mapv transform clause) m) 190 | (mapv transform clause))))) 191 | 192 | (s/def ::symbolic-clause (s/coll-of any? :into [])) 193 | 194 | (s/fdef solve-symbolic-cnf 195 | :args (s/cat :clauses (s/coll-of (s/or :clause ::symbolic-clause 196 | :constraint ::constraint) 197 | :into ())) 198 | :timeout (s/? pos-int?) 199 | :ret (s/nilable ::symbolic-clause)) 200 | 201 | (defn solve-symbolic-cnf 202 | "Takes a formula in symbolic CNF form, i.e., vector of constraints and vectors of variables and 203 | ! of variables, where variables are represented by arbitrary Clojure data. 204 | Also takes optional timeout in millis and optional atom to set to true if timeout occurs. 205 | Returns solution or nil if no solution exists. Stats attached as metadata." 206 | ([clauses] (solve-symbolic-cnf clauses nil nil)) 207 | ([clauses timeout] (solve-symbolic-cnf clauses timeout nil)) 208 | ([clauses timeout timeout-atom] 209 | (b/cond 210 | :let [[object->int int->object] (build-transforms clauses) 211 | transformed-clauses (mapv (clause-transformer object->int) clauses)] 212 | :when-let [solver (create-solver transformed-clauses)] 213 | :let [_ (if timeout 214 | (if *timeout-on-conflicts* 215 | (.setTimeoutOnConflicts solver timeout) 216 | (.setTimeoutMs solver timeout)) 217 | (.setTimeoutOnConflicts solver Integer/MAX_VALUE))] 218 | :when-let [solution (find-model solver timeout-atom)] 219 | :let [untransformed-solution ((clause-transformer int->object) solution)] 220 | (with-meta (vec untransformed-solution) (stats-map solver))))) 221 | 222 | (s/fdef solutions-symbolic-cnf 223 | :args (s/cat :clauses (s/coll-of (s/or :clause ::symbolic-clause 224 | :constraint ::constraint) 225 | :into ())) 226 | :timeout (s/? pos-int?) 227 | :ret (s/* ::symbolic-clause)) 228 | 229 | (defn solutions-symbolic-cnf 230 | "Takes a formula in symbolic CNF form, i.e., vector of constraints and vectors of variables and 231 | ! of variables, where variables are represented by arbitrary Clojure data. 232 | Also takes optional timeout in millis and optional atom to set to true if timeout occurs. 233 | Returns lazy sequence of solutions. Stats attached as metadata to each solution." 234 | ([clauses] (solutions-symbolic-cnf clauses nil nil)) 235 | ([clauses timeout] (solutions-symbolic-cnf clauses timeout nil)) 236 | ([clauses timeout timeout-atom] 237 | (b/cond 238 | :let [[object->int int->object] (build-transforms clauses) 239 | transformed-clauses (mapv (clause-transformer object->int) clauses)] 240 | :when-let [solver (create-solver transformed-clauses)] 241 | :let [iterator (ModelIterator. solver) 242 | _ (if timeout 243 | (if *timeout-on-conflicts* 244 | (.setTimeoutOnConflicts solver timeout) 245 | (.setTimeoutMs solver timeout)) 246 | (.setTimeoutOnConflicts solver Integer/MAX_VALUE)), 247 | solution-iterator (fn [_] 248 | (when-let [next-solution (find-next-model iterator timeout-atom)] 249 | (with-meta (vec next-solution) (stats-map solver))))] 250 | :when-let [first-solution (solution-iterator nil)] 251 | (map (clause-transformer int->object) 252 | (take-while identity (iterate solution-iterator first-solution)))))) 253 | 254 | 255 | ;Tseitin encodings 256 | 257 | (defrecord And [literals]) 258 | (defn AND [& literals] 259 | (->And literals)) 260 | (defn and? [x] 261 | (instance? And x)) 262 | 263 | (defrecord Or [literals]) 264 | (defn OR [& literals] 265 | (->Or literals)) 266 | (defn or? [x] 267 | (instance? Or x)) 268 | 269 | (defrecord Xor [literal1 literal2]) 270 | (def XOR ->Xor) 271 | (defn xor? [x] 272 | (instance? Xor x)) 273 | 274 | (defrecord Imp [literal1 literal2]) 275 | (def IMP ->Imp) 276 | (defn imp? [x] 277 | (instance? Imp x)) 278 | 279 | (defrecord Iff [literal1 literal2]) 280 | (def IFF ->Iff) 281 | (defn iff? [x] 282 | (instance? Iff x)) 283 | 284 | (defn NOR [& literals] 285 | (NOT (apply OR literals))) 286 | 287 | (defn NAND [& literals] 288 | (NOT (apply AND literals))) 289 | 290 | (defn temporary? [var] 291 | (let [literal (if-let [literal (:literal var)] literal var)] 292 | (and (symbol? literal) 293 | (clojure.string/starts-with? (name literal) "temp")))) 294 | 295 | (defn formula? [x] 296 | (contains? #{Not And Or Xor Imp Iff} (type x))) 297 | 298 | (defprotocol CNF 299 | (encode-cnf [this] "Returns variable and clauses")) 300 | 301 | (declare negate) 302 | (extend-protocol CNF 303 | Object 304 | (encode-cnf [this] [this []]) 305 | Not 306 | (encode-cnf [this] 307 | (b/cond 308 | :let [literal (:literal this)] 309 | (not (formula? literal)) [this []] 310 | :let [[v clauses] (encode-cnf literal) 311 | not-v (gensym "temp")] 312 | [not-v (into clauses [[(negate v) (negate not-v)] 313 | [v not-v]])])) 314 | And 315 | (encode-cnf [this] 316 | (let [literals (:literals this), 317 | vs-clauses (map encode-cnf literals), 318 | vs (map first vs-clauses) 319 | clauses (apply concat (map second vs-clauses)), 320 | and-v (gensym "temp")] 321 | [and-v (into clauses (cons 322 | (vec (cons and-v (map negate vs))) 323 | (for [v vs] [(negate and-v) v])))])) 324 | Or 325 | (encode-cnf [this] 326 | (let [literals (:literals this), 327 | vs-clauses (map encode-cnf literals), 328 | vs (map first vs-clauses) 329 | clauses (apply concat (map second vs-clauses)), 330 | or-v (gensym "temp")] 331 | [or-v (into clauses (cons 332 | (vec (cons (negate or-v) vs)) 333 | (for [v vs] [or-v (negate v)])))])) 334 | Xor 335 | (encode-cnf [this] 336 | (let [literal1 (:literal1 this), 337 | literal2 (:literal2 this), 338 | [v1 clauses1] (encode-cnf literal1) 339 | [v2 clauses2] (encode-cnf literal2) 340 | xor-v (gensym "temp")] 341 | [xor-v (into (concat clauses1 clauses2) 342 | [[(negate xor-v) (negate v1) (negate v2)] 343 | [xor-v v1 (negate v2)] 344 | [xor-v (negate v1) v2] 345 | [(negate xor-v) v1 v2]])])) 346 | Imp 347 | (encode-cnf [this] 348 | (let [literal1 (:literal1 this), 349 | literal2 (:literal2 this), 350 | [v1 clauses1] (encode-cnf literal1) 351 | [v2 clauses2] (encode-cnf literal2) 352 | imp-v (gensym "temp")] 353 | [imp-v (into (concat clauses1 clauses2) 354 | [[(negate imp-v) (negate v1) v2] 355 | [imp-v v1] 356 | [imp-v (negate v2)]])])) 357 | 358 | Iff 359 | (encode-cnf [this] 360 | (let [literal1 (:literal1 this), 361 | literal2 (:literal2 this), 362 | [v1 clauses1] (encode-cnf literal1) 363 | [v2 clauses2] (encode-cnf literal2) 364 | iff-v (gensym "temp")] 365 | [iff-v (into (concat clauses1 clauses2) 366 | [[(negate iff-v) v1 (negate v2)] 367 | [iff-v (negate v1) (negate v2)] 368 | [iff-v v1 v2] 369 | [(negate iff-v) (negate v1) v2]])]))) 370 | 371 | (defn formula->cnf [wff] 372 | (let [[v clauses] (encode-cnf wff)] 373 | (conj clauses [v]))) 374 | 375 | (defn- literal? "Is it a plain variable or a NOT of a plain variable?" [wff] 376 | (or (not (formula? wff)) (and (not? wff) (not (formula? (:literal wff)))))) 377 | 378 | (defn encode-constraint "Returns (cons new-constraint new-clauses)" [constraint] 379 | (loop [literals (seq (:literals constraint)), new-literals [], new-clauses []] 380 | (b/cond 381 | (not literals) (cons (assoc constraint :literals new-literals) new-clauses) 382 | :let [literal (first literals)] 383 | (literal? literal) (recur (next literals) (conj new-literals literal) new-clauses) 384 | :let [[new-literal clauses] (encode-cnf literal)] 385 | (recur (next literals) (conj new-literals new-literal) (into new-clauses clauses))))) 386 | 387 | (s/def ::symbolic-formula formula?) 388 | 389 | (s/fdef solve-symbolic-formula 390 | :args (s/cat :formula-or-formulas 391 | (s/alt :single-formula ::symbolic-formula 392 | :multiple-formulas (s/coll-of (s/or :formula ::symbolic-formula 393 | :constraint ::constraint) 394 | :into ())) 395 | :timeout (s/? pos-int?)) 396 | :ret (s/nilable ::symbolic-clause)) 397 | 398 | (defn solve-symbolic-formula 399 | "Takes a formula in symbolic form, i.e., vector of constraints and formulas, where formulas 400 | are built with formula constructors such as AND, OR, XOR, IFF, NAND, NOR, IMP, NOT, 401 | and formula variables are represented by arbitrary Clojure data. 402 | Also takes optional timeout in millis and optional atom to set to true if timeout occurs. 403 | Returns solution or nil if no solution exists. Stats attached as metadata." 404 | ([wffs] (solve-symbolic-formula wffs nil nil)) 405 | ([wffs timeout] (solve-symbolic-formula wffs timeout nil)) 406 | ([wffs timeout timeout-atom] 407 | (b/cond 408 | (not (sequential? wffs)) (recur [wffs] timeout timeout-atom) 409 | :let [{constraints true, wffs false} (group-by constraint? wffs) 410 | cnf (into [] (comp (map formula->cnf) cat) wffs) 411 | clauses (into cnf (mapcat encode-constraint) constraints)] 412 | :when-let [solution (solve-symbolic-cnf clauses timeout timeout-atom)] 413 | (filterv (complement temporary?) solution)))) 414 | 415 | (s/fdef solutions-symbolic-formula 416 | :args (s/cat :formula-or-formulas 417 | (s/alt :single-formula ::symbolic-formula 418 | :multiple-formulas (s/coll-of (s/or :formula ::symbolic-formula 419 | :constraint ::constraint) 420 | :into ())) 421 | :timeout (s/? pos-int?)) 422 | :ret (s/* ::symbolic-clause)) 423 | 424 | (defn solutions-symbolic-formula 425 | "Takes a formula in symbolic form, i.e., vector of constraints and formulas, where formulas 426 | are built with formula constructors such as AND, OR, XOR, IFF, NAND, NOR, IMP, NOT, 427 | and formula variables are represented by arbitrary Clojure data. 428 | Also takes optional timeout in millis and optional atom to set to true if timeout occurs. 429 | Returns lazy sequence of all solutions. Stats attached as metadata." 430 | ([wffs] (solutions-symbolic-formula wffs nil nil)) 431 | ([wffs timeout] (solutions-symbolic-formula wffs timeout nil)) 432 | ([wffs timeout timeout-atom] 433 | (b/cond 434 | (not (sequential? wffs)) (recur [wffs] timeout timeout-atom) 435 | :let [{constraints true, wffs false} (group-by constraint? wffs) 436 | cnf (into [] (comp (map formula->cnf) cat) wffs) 437 | clauses (into cnf (mapcat encode-constraint) constraints)] 438 | :when-let [solutions (solutions-symbolic-cnf clauses timeout timeout-atom)] 439 | (for [solution solutions] 440 | (with-meta (filterv (complement temporary?) solution) (meta solution)))))) 441 | 442 | ; Tools for working with symbolic variables 443 | (def positive? (complement not?)) 444 | (def negative? not?) 445 | (defn negate [x] (if (not? x) (:literal x) (NOT x))) 446 | 447 | (defn true-integer-variables 448 | "Returns a set of all the true variables from a collection of integer variables. 449 | Returns nil if passed nil." 450 | [coll] 451 | (when coll 452 | (into #{} (filter pos?) coll))) 453 | 454 | (defn true-symbolic-variables 455 | "Returns a set of all the true variables from a collection of symbolic variables. 456 | Returns nil if passed nil." 457 | [coll] 458 | (when coll 459 | (into #{} (remove not?) coll))) 460 | --------------------------------------------------------------------------------