UNPKG

logic-solver

Version:

General satisfiability solver for logic problems

1,497 lines (1,328 loc) 62.3 kB
var MiniSat = require("./minisat_wrapper.js"); var _ = require("underscore"); var Logic; Logic = {}; ////////// TYPE TESTERS // Set the `description` property of a tester function and return the function. var withDescription = function (description, tester) { tester.description = description; return tester; }; // Create a function (x) => (x instanceof constructor), but possibly before // constructor is available. For example, if Logic.Formula hasn't been // assigned yet, passing Logic for `obj` and "Formula" for `constructorName` // will still work. var lazyInstanceofTester = function (description, obj, constructorName) { return withDescription(description, function (x) { return x instanceof obj[constructorName]; }); }; ///// PUBLIC TYPE TESTERS // All variables have a name and a number. The number is mainly used // internally, and it's what's given to MiniSat. Names and numbers // are interchangeable, which is convenient for doing manipulation // of terms in a way that works before or after variable names are // converted to numbers. // Term: a variable name or variable number, optionally // negated (meaning "boolean not"). For example, // `1`, `-1`, `"foo"`, or `"-foo"`. All variables have // internal numbers that start at 1, so "foo" might be // variable number 1, for example. Any number of leading // "-" will be parsed in the string form, but we try to // keep it to either one or zero of them. Logic.isNumTerm = withDescription('a NumTerm (non-zero integer)', function (x) { // 32-bit integer, but not 0 return (x === (x | 0)) && x !== 0; }); // NameTerm must not be empty, or just `-` characters, or look like a // number. Specifically, it can't be zero or more `-` followed by // zero or more digits. Logic.isNameTerm = withDescription('a NameTerm (string)', function (x) { return (typeof x === 'string') && ! /^-*[0-9]*$/.test(x); }); Logic.isTerm = withDescription('a Term (appropriate string or number)', function (x) { return Logic.isNumTerm(x) || Logic.isNameTerm(x); }); // WholeNumber: a non-negative integer (0 is allowed) Logic.isWholeNumber = withDescription('a whole number (integer >= 0)', function (x) { return (x === (x | 0)) && x >= 0; }); Logic.isFormula = lazyInstanceofTester('a Formula', Logic, 'Formula'); Logic.isClause = lazyInstanceofTester('a Clause', Logic, 'Clause'); Logic.isBits = lazyInstanceofTester('a Bits', Logic, 'Bits'); ///// UNDOCUMENTED TYPE TESTERS Logic._isInteger = withDescription( 'an integer', function (x) { return x === (x | 0); }); Logic._isFunction = withDescription( 'a Function', function (x) { return typeof x === 'function'; }); Logic._isString = withDescription( 'a String', function (x) { return typeof x === 'string'; }); Logic._isArrayWhere = function (tester) { var description = 'an array'; if (tester.description) { description += ' of ' + tester.description; } return withDescription(description, function (x) { if (! _.isArray(x)) { return false; } else { for (var i = 0; i < x.length; i++) { if (! tester(x[i])) { return false; } } return true; } }); }; Logic._isFormulaOrTerm = withDescription('a Formula or Term', function (x) { return Logic.isFormula(x) || Logic.isTerm(x); }); Logic._isFormulaOrTermOrBits = withDescription('a Formula, Term, or Bits', function (x) { return Logic.isFormula(x) || Logic.isBits(x) || Logic.isTerm(x); }); Logic._MiniSat = MiniSat; // Expose for testing and poking around // import the private testers from types.js var isInteger = Logic._isInteger; var isFunction = Logic._isFunction; var isString = Logic._isString; var isArrayWhere = Logic._isArrayWhere; var isFormulaOrTerm = Logic._isFormulaOrTerm; var isFormulaOrTermOrBits = Logic._isFormulaOrTermOrBits; Logic._assert = function (value, tester, description) { if (! tester(value)) { var displayValue = (typeof value === 'string' ? JSON.stringify(value) : value); throw new Error(displayValue + " is not " + (tester.description || description)); } }; // Call this as `if (assert) assertNumArgs(...)` var assertNumArgs = function (actual, expected, funcName) { if (actual !== expected) { throw new Error("Expected " + expected + " args in " + funcName + ", got " + actual); } }; // Call `assert` as: `if (assert) assert(...)`. // This local variable temporarily set to `null` inside // `Logic.disablingAssertions`. var assert = Logic._assert; // Like `if (assert) assert(...)` but usable from other files in the package. Logic._assertIfEnabled = function (value, tester, description) { if (assert) assert(value, tester, description); }; // Disabling runtime assertions speeds up clause generation. Assertions // are disabled when the local variable `assert` is null instead of // `Logic._assert`. Logic.disablingAssertions = function (f) { var oldAssert = assert; try { assert = null; return f(); } finally { assert = oldAssert; } }; // Back-compat. Logic._disablingTypeChecks = Logic.disablingAssertions; //////////////////// // Takes a Formula or Term, returns a Formula or Term. // Unlike other operators, if you give it a Term, // you will get a Term back (of the same type, NameTerm // or NumTerm). Logic.not = function (operand) { if (assert) assert(operand, isFormulaOrTerm); if (operand instanceof Logic.Formula) { return new Logic.NotFormula(operand); } else { // Term if (typeof operand === 'number') { return -operand; } else if (operand.charAt(0) === '-') { return operand.slice(1); } else { return '-' + operand; } } }; Logic.NAME_FALSE = "$F"; Logic.NAME_TRUE = "$T"; Logic.NUM_FALSE = 1; Logic.NUM_TRUE = 2; Logic.TRUE = Logic.NAME_TRUE; Logic.FALSE = Logic.NAME_FALSE; // Abstract base class. Subclasses are created using _defineFormula. Logic.Formula = function () {}; Logic._defineFormula = function (constructor, typeName, methods) { if (assert) assert(constructor, isFunction); if (assert) assert(typeName, isString); constructor.prototype = new Logic.Formula(); constructor.prototype.type = typeName; if (methods) { _.extend(constructor.prototype, methods); } }; // Returns a list of Clauses that together require the Formula to be // true, or false (depending on isTrue; both cases must be // implemented). A single Clause may also be returned. The // implementation should call the termifier to convert terms and // formulas to NumTerms specific to a solver instance, and use them to // construct a Logic.Clause. Logic.Formula.prototype.generateClauses = function (isTrue, termifier) { throw new Error("Cannot generate this Formula; it must be expanded"); }; // All Formulas have a globally-unique id so that Solvers can track them. // It is assigned lazily. Logic.Formula._nextGuid = 1; Logic.Formula.prototype._guid = null; Logic.Formula.prototype.guid = function () { if (this._guid === null) { this._guid = Logic.Formula._nextGuid++; } return this._guid; }; // A "clause" is a disjunction of terms, e.g. "A or B or (not C)", // which we write "A v B v -C". Logic.Clause is mainly an internal // Solver data structure, which is the final result of formula // generation and mapping variable names to numbers, before passing // the clauses to MiniSat. Logic.Clause = function (/*formulaOrArray, ...*/) { var terms = _.flatten(arguments); if (assert) assert(terms, isArrayWhere(Logic.isNumTerm)); this.terms = terms; // immutable [NumTerm] }; // Returns a new Clause with the extra term or terms appended Logic.Clause.prototype.append = function (/*formulaOrArray, ...*/) { return new Logic.Clause(this.terms.concat(_.flatten(arguments))); }; var FormulaInfo = function () { // We generate a variable when a Formula is used. this.varName = null; // string name of variable this.varNum = null; // number of variable (always positive) // A formula variable that is used only in the positive or only // in the negative doesn't need the full set of clauses that // establish a bidirectional implication between the formula and the // variable. For example, in the formula `Logic.or("A", "B")`, with the // formula variable `$or1`, the full set of clauses is `A v B v // -$or1; -A v $or1; -B v $or1`. If both `$or1` and `-$or1` appear // elsewhere in the set of clauses, then all three of these clauses // are required. However, somewhat surprisingly, if only `$or1` appears, // then only the first is necessary. If only `-$or1` appears, then only // the second and third are necessary. // // Suppose the formula A v B is represented by the variable $or1, // and $or1 is only used positively. It's important that A v B being // false forces $or1 to be false, so that when $or1 is used it has // the appropriate effect. For example, if we have the clause $or1 v // C, then A v B being false should force $or1 to be false, which // forces C to be true. So we generate the clause A v B v // -$or1. (The implications of this clause are: If A v B is false, // $or1 must be false. If $or1 is true, A v B must be true.) // // However, in the case where A v B is true, we don't actually // need to insist that the solver set $or1 to true, as long as we // are ok with relaxing the relationship between A v B and $or1 // and getting a "wrong" value for $or1 in the solution. Suppose // the solver goes to work and at some point determines A v B to // be true. It could set $or1 to true, satisfying all the clauses // where it appears, or it could set $or1 to false, which only // constrains the solution space and doesn't open up any new // solutions for other variables. If the solver happens to find a // solution where A v B is true and $or1 is false, we know there // is a similar solution that makes all the same assignments // except it assigns $or1 to true. // // If a formula is used only negatively, a similar argument applies // but with signs flipped, and if it is used both positively and // negatively, both kinds of clauses must be generated. // // See the mention of "polarity" in the MiniSat+ paper // (http://minisat.se/downloads/MiniSat+.pdf). // // These flags are set when generation has been done for the positive // case or the negative case, so that we only generate each one once. this.occursPositively = false; this.occursNegatively = false; // If a Formula has been directly required or forbidden, we can // replace it by TRUE or FALSE in subsequent clauses. Track the // information here. this.isRequired = false; this.isForbidden = false; }; // The "termifier" interface is provided to a Formula's // generateClauses method, which must use it to generate Clause // objects. // // The reason for this approach is that it gives the Formula control // over the clauses returned, but it gives the Solver control over // Formula generation. Logic.Termifier = function (solver) { this.solver = solver; }; // The main entry point, the `clause` method takes a list of // FormulaOrTerms and converts it to a Clause containing NumTerms, *by // replacing Formulas with their variables*, creating the variable if // necessary. For example, if an OrFormula is represented by the // variable `$or1`, it will be replaced by the numeric version of // `$or1` to make the Clause. When the Clause is actually used, it // will trigger generation of the clauses that relate `$or1` to the // operands of the OrFormula. Logic.Termifier.prototype.clause = function (/*args*/) { var self = this; var formulas = _.flatten(arguments); if (assert) assert(formulas, isArrayWhere(isFormulaOrTerm)); return new Logic.Clause(_.map(formulas, function (f) { return self.term(f); })); }; // The `term` method performs the mapping from FormulaOrTerm to // NumTerm. It's called by `clause` and could be called directly // from a Formula's generateClauses if it was useful for some // reason. Logic.Termifier.prototype.term = function (formula) { return this.solver._formulaToTerm(formula); }; // The `generate` method generates clauses for a Formula (or // Term). It should be used carefully, because it works quite // differently from passing a Formula into `clause`, which is the // normal way for one Formula to refer to another. When you use a // Formula in `clause`, it is replaced by the Formula's variable, // and the Solver handles generating the Formula's clauses once. // When you use `generate`, this system is bypassed, and the // Formula's generateClauses method is called pretty much directly, // returning the array of Clauses. Logic.Termifier.prototype.generate = function (isTrue, formula) { return this.solver._generateFormula(isTrue, formula, this); }; Logic.Solver = function () { var self = this; self.clauses = []; // mutable [Clause] self._num2name = [null]; // no 0th var self._name2num = {}; // (' '+vname) -> vnum // true and false var F = self.getVarNum(Logic.NAME_FALSE, false, true); // 1 var T = self.getVarNum(Logic.NAME_TRUE, false, true); // 2 if (F !== Logic.NUM_FALSE || T !== Logic.NUM_TRUE) { throw new Error("Assertion failure: $T and $F have wrong numeric value"); } self._F_used = false; self._T_used = false; // It's important that these clauses are elements 0 and 1 // of the clauses array, so that they can optionally be stripped // off. For example, _clauseData takes advantage of this fact. self.clauses.push(new Logic.Clause(-Logic.NUM_FALSE)); self.clauses.push(new Logic.Clause(Logic.NUM_TRUE)); self._formulaInfo = {}; // Formula guid -> FormulaInfo // For generating formula variables like "$or1", "$or2", "$and1", "$and2" self._nextFormulaNumByType = {}; // Formula type -> next var id // Map of Formulas whose info has `false` for either // `occursPositively` or `occursNegatively` self._ungeneratedFormulas = {}; // varNum -> Formula self._numClausesAddedToMiniSat = 0; self._unsat = false; // once true, no solution henceforth self._minisat = new MiniSat(); // this takes some time self._termifier = new Logic.Termifier(self); }; // Get a var number for vname, assigning it a number if it is new. // Setting "noCreate" to true causes the function to return 0 instead of // creating a new variable. // Setting "_createInternals" to true grants the ability to create $ variables. Logic.Solver.prototype.getVarNum = function (vname, noCreate, _createInternals) { var key = ' '+vname; if (_.has(this._name2num, key)) { return this._name2num[key]; } else if (noCreate) { return 0; } else { if (vname.charAt(0) === "$" && ! _createInternals) { throw new Error("Only generated variable names can start with $"); } var vnum = this._num2name.length; this._name2num[key] = vnum; this._num2name.push(vname); return vnum; } }; Logic.Solver.prototype.getVarName = function (vnum) { if (assert) assert(vnum, isInteger); var num2name = this._num2name; if (vnum < 1 || vnum >= num2name.length) { throw new Error("Bad variable num: " + vnum); } else { return num2name[vnum]; } }; // Converts a Term to a NumTerm (if it isn't already). This is done // when a Formula creates Clauses for a Solver, since Clauses require // NumTerms. NumTerms stay the same, while a NameTerm like "-foo" // might become (say) the number -3. If a NameTerm names a variable // that doesn't exist, it is automatically created, unless noCreate // is passed, in which case 0 is returned instead. Logic.Solver.prototype.toNumTerm = function (t, noCreate) { var self = this; if (assert) assert(t, Logic.isTerm); if (typeof t === 'number') { return t; } else { // string var not = false; while (t.charAt(0) === '-') { t = t.slice(1); not = ! not; } var n = self.getVarNum(t, noCreate); if (! n) { return 0; // must be the noCreate case } else { return (not ? -n : n); } } }; // Converts a Term to a NameTerm (if it isn't already). Logic.Solver.prototype.toNameTerm = function (t) { var self = this; if (assert) assert(t, Logic.isTerm); if (typeof t === 'string') { // canonicalize, removing leading "--" while (t.slice(0, 2) === '--') { t = t.slice(2); } return t; } else { // number var not = false; if (t < 0) { not = true; t = -t; } t = self.getVarName(t); if (not) { t = '-' + t; } return t; } }; Logic.Solver.prototype._addClause = function (cls, _extraTerms, _useTermOverride) { var self = this; if (assert) assert(cls, Logic.isClause); var extraTerms = null; if (_extraTerms) { extraTerms = _extraTerms; if (assert) assert(extraTerms, isArrayWhere(Logic.isNumTerm)); } var usedF = false; var usedT = false; var numRealTerms = cls.terms.length; if (extraTerms) { // extraTerms are added to the clause as is. Formula variables in // extraTerms do not cause Formula clause generation, which is // necessary to implement Formula clause generation. cls = cls.append(extraTerms); } for (var i = 0; i < cls.terms.length; i++) { var t = cls.terms[i]; var v = (t < 0) ? -t : t; if (v === Logic.NUM_FALSE) { usedF = true; } else if (v === Logic.NUM_TRUE) { usedT = true; } else if (v < 1 || v >= self._num2name.length) { throw new Error("Bad variable number: " + v); } else if (i < numRealTerms) { if (_useTermOverride) { _useTermOverride(t); } else { self._useFormulaTerm(t); } } } this._F_used = (this._F_used || usedF); this._T_used = (this._T_used || usedT); this.clauses.push(cls); }; // When we actually use a Formula variable, generate clauses for it, // based on whether the usage is positive or negative. For example, // if the Formula `Logic.or("X", "Y")` is represented by `$or1`, which // is variable number 5, then when you actually use 5 or -5 in a clause, // the clauses "X v Y v -5" (when you use 5) or "-X v 5; -Y v 5" // (when you use -5) will be generated. The clause "X v Y v -5" // is equivalent to "5 => X v Y" (or -(X v Y) => -5), while the clauses // "-X v 5; -Y v 5" are equivalent to "-5 => -X; -5 => -Y" (or // "X => 5; Y => 5"). Logic.Solver.prototype._useFormulaTerm = function (t, _addClausesOverride) { var self = this; if (assert) assert(t, Logic.isNumTerm); var v = (t < 0) ? -t : t; if (! _.has(self._ungeneratedFormulas, v)) { return; } // using a Formula's var; maybe have to generate clauses // for the Formula var formula = self._ungeneratedFormulas[v]; var info = self._getFormulaInfo(formula); var positive = t > 0; // To avoid overflowing the JS stack, defer calls to addClause. // The way we get overflows is when Formulas are deeply nested // (which happens naturally when you call Logic.sum or // Logic.weightedSum on a long list of terms), which causes // addClause to call useFormulaTerm to call addClause, and so // on. Approach: The outermost useFormulaTerm keeps a list // of clauses to add, and then adds them in a loop using a // special argument to addClause that passes a special argument // to useFormulaTerm that causes those clauses to go into the // list too. Code outside of `_useFormulaTerm` and `_addClause(s)` // does not have to pass these special arguments to call them. var deferredAddClauses = null; var addClauses; if (! _addClausesOverride) { deferredAddClauses = []; addClauses = function (clauses, extraTerms) { deferredAddClauses.push({clauses: clauses, extraTerms: extraTerms}); }; } else { addClauses = _addClausesOverride; } if (positive && ! info.occursPositively) { // generate clauses for the formula. // Eg, if we use variable `X` which represents the formula // `A v B`, add the clause `A v B v -X`. // By using the extraTerms argument to addClauses, we avoid // treating this as a negative occurrence of X. info.occursPositively = true; var clauses = self._generateFormula(true, formula); addClauses(clauses, [-v]); } else if ((! positive) && ! info.occursNegatively) { // Eg, if we have the term `-X` where `X` represents the // formula `A v B`, add the clauses `-A v X` and `-B v X`. // By using the extraTerms argument to addClauses, we avoid // treating this as a positive occurrence of X. info.occursNegatively = true; var clauses = self._generateFormula(false, formula); addClauses(clauses, [v]); } if (info.occursPositively && info.occursNegatively) { delete self._ungeneratedFormulas[v]; } if (! (deferredAddClauses && deferredAddClauses.length)) { return; } var useTerm = function (t) { self._useFormulaTerm(t, addClauses); }; // This is the loop that turns recursion into iteration. // When addClauses calls useTerm, which calls useFormulaTerm, // the nested useFormulaTerm will add any clauses to our // own deferredAddClauses list. while (deferredAddClauses.length) { var next = deferredAddClauses.pop(); self._addClauses(next.clauses, next.extraTerms, useTerm); } }; Logic.Solver.prototype._addClauses = function (array, _extraTerms, _useTermOverride) { if (assert) assert(array, isArrayWhere(Logic.isClause)); var self = this; _.each(array, function (cls) { self._addClause(cls, _extraTerms, _useTermOverride); }); }; Logic.Solver.prototype.require = function (/*formulaOrArray, ...*/) { this._requireForbidImpl(true, _.flatten(arguments)); }; Logic.Solver.prototype.forbid = function (/*formulaOrArray, ...*/) { this._requireForbidImpl(false, _.flatten(arguments)); }; Logic.Solver.prototype._requireForbidImpl = function (isRequire, formulas) { var self = this; if (assert) assert(formulas, isArrayWhere(isFormulaOrTerm)); _.each(formulas, function (f) { if (f instanceof Logic.NotFormula) { self._requireForbidImpl(!isRequire, [f.operand]); } else if (f instanceof Logic.Formula) { var info = self._getFormulaInfo(f); if (info.varNum !== null) { var sign = isRequire ? 1 : -1; self._addClause(new Logic.Clause(sign*info.varNum)); } else { self._addClauses(self._generateFormula(isRequire, f)); } if (isRequire) { info.isRequired = true; } else { info.isForbidden = true; } } else { self._addClauses(self._generateFormula(isRequire, f)); } }); }; Logic.Solver.prototype._generateFormula = function (isTrue, formula, _termifier) { var self = this; if (assert) assert(formula, isFormulaOrTerm); if (formula instanceof Logic.NotFormula) { return self._generateFormula(!isTrue, formula.operand); } else if (formula instanceof Logic.Formula) { var info = self._getFormulaInfo(formula); if ((isTrue && info.isRequired) || (!isTrue && info.isForbidden)) { return []; } else if ((isTrue && info.isForbidden) || (!isTrue && info.isRequired)) { return [new Logic.Clause()]; // never satisfied clause } else { var ret = formula.generateClauses(isTrue, _termifier || self._termifier); return _.isArray(ret) ? ret : [ret]; } } else { // Term var t = self.toNumTerm(formula); var sign = isTrue ? 1 : -1; if (t === sign*Logic.NUM_TRUE || t === -sign*Logic.NUM_FALSE) { return []; } else if (t === sign*Logic.NUM_FALSE || t === -sign*Logic.NUM_TRUE) { return [new Logic.Clause()]; // never satisfied clause } else { return [new Logic.Clause(sign*t)]; } } }; // Get clause data as an array of arrays of integers, // for testing and debugging purposes. Logic.Solver.prototype._clauseData = function () { var clauses = _.pluck(this.clauses, 'terms'); if (! this._T_used) { clauses.splice(1, 1); } if (! this._F_used) { clauses.splice(0, 1); } return clauses; }; // Get clause data as an array of human-readable strings, // for testing and debugging purposes. // A clause might look like "A v -B" (where "v" represents // and OR operator). Logic.Solver.prototype._clauseStrings = function () { var self = this; var clauseData = self._clauseData(); return _.map(clauseData, function (clause) { return _.map(clause, function (nterm) { var str = self.toNameTerm(nterm); if (/\s/.test(str)) { // write name in quotes for readability. we don't bother // making this string machine-parsable in the general case. var sign = ''; if (str.charAt(0) === '-') { // temporarily remove '-' sign = '-'; str = str.slice(1); } str = sign + '"' + str + '"'; } return str; }).join(' v '); }); }; Logic.Solver.prototype._getFormulaInfo = function (formula, _noCreate) { var self = this; var guid = formula.guid(); if (! self._formulaInfo[guid]) { if (_noCreate) { return null; } self._formulaInfo[guid] = new FormulaInfo(); } return self._formulaInfo[guid]; }; // Takes a Formula or an array of Formulas, returns a NumTerm or // array of NumTerms. Logic.Solver.prototype._formulaToTerm = function (formula) { var self = this; if (_.isArray(formula)) { if (assert) assert(formula, isArrayWhere(isFormulaOrTerm)); return _.map(formula, _.bind(self._formulaToTerm, self)); } else { if (assert) assert(formula, isFormulaOrTerm); } if (formula instanceof Logic.NotFormula) { // shortcut that avoids creating a variable called // something like "$not1" when you use Logic.not(formula). return Logic.not(self._formulaToTerm(formula.operand)); } else if (formula instanceof Logic.Formula) { var info = this._getFormulaInfo(formula); if (info.isRequired) { return Logic.NUM_TRUE; } else if (info.isForbidden) { return Logic.NUM_FALSE; } else if (info.varNum === null) { // generate a Solver-local formula variable like "$or1" var type = formula.type; if (! this._nextFormulaNumByType[type]) { this._nextFormulaNumByType[type] = 1; } var numForVarName = this._nextFormulaNumByType[type]++; info.varName = "$" + formula.type + numForVarName; info.varNum = this.getVarNum(info.varName, false, true); this._ungeneratedFormulas[info.varNum] = formula; } return info.varNum; } else { // formula is a Term return self.toNumTerm(formula); } }; Logic.or = function (/*formulaOrArray, ...*/) { var args = _.flatten(arguments); if (args.length === 0) { return Logic.FALSE; } else if (args.length === 1) { if (assert) assert(args[0], isFormulaOrTerm); return args[0]; } else { return new Logic.OrFormula(args); } }; Logic.OrFormula = function (operands) { if (assert) assert(operands, isArrayWhere(isFormulaOrTerm)); this.operands = operands; }; Logic._defineFormula(Logic.OrFormula, 'or', { generateClauses: function (isTrue, t) { if (isTrue) { // eg A v B v C return t.clause(this.operands); } else { // eg -A; -B; -C var result = []; _.each(this.operands, function (o) { result.push.apply(result, t.generate(false, o)); }); return result; } } }); Logic.NotFormula = function (operand) { if (assert) assert(operand, isFormulaOrTerm); this.operand = operand; }; // No generation or simplification for 'not'; it is // simplified away by the solver itself. Logic._defineFormula(Logic.NotFormula, 'not'); Logic.and = function (/*formulaOrArray, ...*/) { var args = _.flatten(arguments); if (args.length === 0) { return Logic.TRUE; } else if (args.length === 1) { if (assert) assert(args[0], isFormulaOrTerm); return args[0]; } else { return new Logic.AndFormula(args); } }; Logic.AndFormula = function (operands) { if (assert) assert(operands, isArrayWhere(isFormulaOrTerm)); this.operands = operands; }; Logic._defineFormula(Logic.AndFormula, 'and', { generateClauses: function (isTrue, t) { if (isTrue) { // eg A; B; C var result = []; _.each(this.operands, function (o) { result.push.apply(result, t.generate(true, o)); }); return result; } else { // eg -A v -B v -C return t.clause(_.map(this.operands, Logic.not)); } } }); // Group `array` into groups of N, where the last group // may be shorter than N. group([a,b,c,d,e], 3) => [[a,b,c],[d,e]] var group = function (array, N) { var ret = []; for (var i = 0; i < array.length; i += N) { ret.push(array.slice(i, i+N)); } return ret; }; Logic.xor = function (/*formulaOrArray, ...*/) { var args = _.flatten(arguments); if (args.length === 0) { return Logic.FALSE; } else if (args.length === 1) { if (assert) assert(args[0], isFormulaOrTerm); return args[0]; } else { return new Logic.XorFormula(args); } }; Logic.XorFormula = function (operands) { if (assert) assert(operands, isArrayWhere(isFormulaOrTerm)); this.operands = operands; }; Logic._defineFormula(Logic.XorFormula, 'xor', { generateClauses: function (isTrue, t) { var args = this.operands; var not = Logic.not; if (args.length > 3) { return t.generate( isTrue, Logic.xor( _.map(group(this.operands, 3), function (group) { return Logic.xor(group); }))); } else if (isTrue) { // args.length <= 3 if (args.length === 0) { return t.clause(); // always fail } else if (args.length === 1) { return t.clause(args[0]); } else if (args.length === 2) { var A = args[0], B = args[1]; return [t.clause(A, B), // A v B t.clause(not(A), not(B))]; // -A v -B } else if (args.length === 3) { var A = args[0], B = args[1], C = args[2]; return [t.clause(A, B, C), // A v B v C t.clause(A, not(B), not(C)), // A v -B v -C t.clause(not(A), B, not(C)), // -A v B v -C t.clause(not(A), not(B), C)]; // -A v -B v C } } else { // !isTrue, args.length <= 3 if (args.length === 0) { return []; // always succeed } else if (args.length === 1) { return t.clause(not(args[0])); } else if (args.length === 2) { var A = args[0], B = args[1]; return [t.clause(A, not(B)), // A v -B t.clause(not(A), B)]; // -A v B } else if (args.length === 3) { var A = args[0], B = args[1], C = args[2]; return [t.clause(not(A), not(B), not(C)), // -A v -B v -C t.clause(not(A), B, C), // -A v B v C t.clause(A, not(B), C), // A v -B v C t.clause(A, B, not(C))]; // A v B v -C } } } }); Logic.atMostOne = function (/*formulaOrArray, ...*/) { var args = _.flatten(arguments); if (args.length <= 1) { return Logic.TRUE; } else { return new Logic.AtMostOneFormula(args); } }; Logic.AtMostOneFormula = function (operands) { if (assert) assert(operands, isArrayWhere(isFormulaOrTerm)); this.operands = operands; }; Logic._defineFormula(Logic.AtMostOneFormula, 'atMostOne', { generateClauses: function (isTrue, t) { var args = this.operands; var not = Logic.not; if (args.length <= 1) { return []; // always succeed } else if (args.length === 2) { return t.generate(isTrue, Logic.not(Logic.and(args))); } else if (isTrue && args.length === 3) { // Pick any two args; at least one is false (they aren't // both true). This strategy would also work for // N>3, and could provide a speed-up by having more clauses // (N^2) but fewer propagation steps. No speed-up was // observed on the Sudoku test from using this strategy // up to N=10. var clauses = []; for (var i = 0; i < args.length; i++) { for (var j = i+1; j < args.length; j++) { clauses.push(t.clause(not(args[i]), not(args[j]))); } } return clauses; } else if ((! isTrue) && args.length === 3) { var A = args[0], B = args[1], C = args[2]; // Pick any two args; at least one is true (they aren't // both false). This only works for N=3. return [t.clause(A, B), t.clause(A, C), t.clause(B, C)]; } else { // See the "commander variables" technique from: // http://www.cs.cmu.edu/~wklieber/papers/2007_efficient-cnf-encoding-for-selecting-1.pdf // But in short: At most one group has at least one "true", // and each group has at most one "true". Formula generation // automatically generates the right implications. var groups = group(args, 3); var ors = _.map(groups, function (g) { return Logic.or(g); }); if (groups[groups.length - 1].length < 2) { // Remove final group of length 1 so we don't generate // no-op clauses of one sort or another groups.pop(); } var atMostOnes = _.map(groups, function (g) { return Logic.atMostOne(g); }); return t.generate(isTrue, Logic.and(Logic.atMostOne(ors), atMostOnes)); } } }); Logic.implies = function (A, B) { if (assert) assertNumArgs(arguments.length, 2, "Logic.implies"); return new Logic.ImpliesFormula(A, B); }; Logic.ImpliesFormula = function (A, B) { if (assert) assert(A, isFormulaOrTerm); if (assert) assert(B, isFormulaOrTerm); if (assert) assertNumArgs(arguments.length, 2, "Logic.implies"); this.A = A; this.B = B; }; Logic._defineFormula(Logic.ImpliesFormula, 'implies', { generateClauses: function (isTrue, t) { return t.generate(isTrue, Logic.or(Logic.not(this.A), this.B)); } }); Logic.equiv = function (A, B) { if (assert) assertNumArgs(arguments.length, 2, "Logic.equiv"); return new Logic.EquivFormula(A, B); }; Logic.EquivFormula = function (A, B) { if (assert) assert(A, isFormulaOrTerm); if (assert) assert(B, isFormulaOrTerm); if (assert) assertNumArgs(arguments.length, 2, "Logic.equiv"); this.A = A; this.B = B; }; Logic._defineFormula(Logic.EquivFormula, 'equiv', { generateClauses: function (isTrue, t) { return t.generate(!isTrue, Logic.xor(this.A, this.B)); } }); Logic.exactlyOne = function (/*formulaOrArray, ...*/) { var args = _.flatten(arguments); if (args.length === 0) { return Logic.FALSE; } else if (args.length === 1) { if (assert) assert(args[0], isFormulaOrTerm); return args[0]; } else { return new Logic.ExactlyOneFormula(args); } }; Logic.ExactlyOneFormula = function (operands) { if (assert) assert(operands, isArrayWhere(isFormulaOrTerm)); this.operands = operands; }; Logic._defineFormula(Logic.ExactlyOneFormula, 'exactlyOne', { generateClauses: function (isTrue, t) { var args = this.operands; if (args.length < 3) { return t.generate(isTrue, Logic.xor(args)); } else { return t.generate(isTrue, Logic.and(Logic.atMostOne(args), Logic.or(args))); } } }); // List of 0 or more formulas or terms, which together represent // a non-negative integer. Least significant bit is first. That is, // the kth array element has a place value of 2^k. Logic.Bits = function (formulaArray) { if (assert) assert(formulaArray, isArrayWhere(isFormulaOrTerm)); this.bits = formulaArray; // public, immutable }; Logic.constantBits = function (wholeNumber) { if (assert) assert(wholeNumber, Logic.isWholeNumber); var result = []; while (wholeNumber) { result.push((wholeNumber & 1) ? Logic.TRUE : Logic.FALSE); wholeNumber >>>= 1; } return new Logic.Bits(result); }; Logic.variableBits = function (baseName, nbits) { if (assert) assert(nbits, Logic.isWholeNumber); var result = []; for (var i = 0; i < nbits; i++) { result.push(baseName + '$' + i); } return new Logic.Bits(result); }; // bits1 <= bits2 Logic.lessThanOrEqual = function (bits1, bits2) { return new Logic.LessThanOrEqualFormula(bits1, bits2); }; Logic.LessThanOrEqualFormula = function (bits1, bits2) { if (assert) assert(bits1, Logic.isBits); if (assert) assert(bits2, Logic.isBits); if (assert) assertNumArgs(arguments.length, 2, "Bits comparison function"); this.bits1 = bits1; this.bits2 = bits2; }; var genLTE = function (bits1, bits2, t, notEqual) { var ret = []; // clone so we can mutate them in place var A = bits1.bits.slice(); var B = bits2.bits.slice(); if (notEqual && ! bits2.bits.length) { // can't be less than 0 return t.clause(); } // if A is longer than B, the extra (high) bits // must be 0. while (A.length > B.length) { var hi = A.pop(); ret.push(t.clause(Logic.not(hi))); } // now B.length >= A.length // Let xors[i] be (A[i] xor B[i]), or just // B[i] if A is too short. var xors = _.map(B, function (b, i) { if (i < A.length) { return Logic.xor(A[i], b); } else { return b; } }); // Suppose we are comparing 3-bit numbers, requiring // that ABC <= XYZ. Here is what we require: // // * It is false that A=1 and X=0. // * It is false that A=X, B=1, and Y=0. // * It is false that A=X, B=Y, C=1, and Y=0. // // Translating these into clauses using DeMorgan's law: // // * A=0 or X=1 // * (A xor X) or B=0 or Y=1 // * (A xor X) or (B xor Y) or C=0 or Y=1 // // Since our arguments are LSB first, in the example // we would be given [C, B, A] and [Z, Y, X] as input. // We iterate over the first argument starting from // the right, and build up a clause by iterating over // the xors from the right. // // If we have ABC <= VWXYZ, then we still have three clauses, // but each one is prefixed with "V or W or", because V and W // are at the end of the xors array. This is equivalent to // padding ABC with two zeros. for (var i = A.length-1; i >= 0; i--) { ret.push(t.clause(xors.slice(i+1), Logic.not(A[i]), B[i])); } if (notEqual) { ret.push.apply(ret, t.generate(true, Logic.or(xors))); } return ret; }; Logic._defineFormula(Logic.LessThanOrEqualFormula, 'lte', { generateClauses: function (isTrue, t) { if (isTrue) { // bits1 <= bits2 return genLTE(this.bits1, this.bits2, t, false); } else { // bits2 < bits1 return genLTE(this.bits2, this.bits1, t, true); } } }); // bits1 < bits2 Logic.lessThan = function (bits1, bits2) { return new Logic.LessThanFormula(bits1, bits2); }; Logic.LessThanFormula = function (bits1, bits2) { if (assert) assert(bits1, Logic.isBits); if (assert) assert(bits2, Logic.isBits); if (assert) assertNumArgs(arguments.length, 2, "Bits comparison function"); this.bits1 = bits1; this.bits2 = bits2; }; Logic._defineFormula(Logic.LessThanFormula, 'lt', { generateClauses: function (isTrue, t) { if (isTrue) { // bits1 < bits2 return genLTE(this.bits1, this.bits2, t, true); } else { // bits2 <= bits1 return genLTE(this.bits2, this.bits1, t, false); } } }); Logic.greaterThan = function (bits1, bits2) { return Logic.lessThan(bits2, bits1); }; Logic.greaterThanOrEqual = function (bits1, bits2) { return Logic.lessThanOrEqual(bits2, bits1); }; Logic.equalBits = function (bits1, bits2) { return new Logic.EqualBitsFormula(bits1, bits2); }; Logic.EqualBitsFormula = function (bits1, bits2) { if (assert) assert(bits1, Logic.isBits); if (assert) assert(bits2, Logic.isBits); if (assert) assertNumArgs(arguments.length, 2, "Logic.equalBits"); this.bits1 = bits1; this.bits2 = bits2; }; Logic._defineFormula(Logic.EqualBitsFormula, 'equalBits', { generateClauses: function (isTrue, t) { var A = this.bits1.bits; var B = this.bits2.bits; var nbits = Math.max(A.length, B.length); var facts = []; for (var i = 0; i < nbits; i++) { if (i >= A.length) { facts.push(Logic.not(B[i])); } else if (i >= B.length) { facts.push(Logic.not(A[i])); } else { facts.push(Logic.equiv(A[i], B[i])); } } return t.generate(isTrue, Logic.and(facts)); } }); // Definition of full-adder and half-adder: // // A full-adder is a 3-input, 2-output gate producing the sum of its // inputs as a 2-bit binary number. The most significant bit is called // "carry", the least significant "sum". A half-adder does the same // thing, but has only 2 inputs (and can therefore never output a // "3"). // // The half-adder sum bit is really just an XOR, and the carry bit // is really just an AND. However, they get their own formula types // here to enhance readability of the generated clauses. Logic.HalfAdderSum = function (formula1, formula2) { if (assert) assert(formula1, isFormulaOrTerm); if (assert) assert(formula2, isFormulaOrTerm); if (assert) assertNumArgs(arguments.length, 2, "Logic.HalfAdderSum"); this.a = formula1; this.b = formula2; }; Logic._defineFormula(Logic.HalfAdderSum, 'hsum', { generateClauses: function (isTrue, t) { return t.generate(isTrue, Logic.xor(this.a, this.b)); } }); Logic.HalfAdderCarry = function (formula1, formula2) { if (assert) assert(formula1, isFormulaOrTerm); if (assert) assert(formula2, isFormulaOrTerm); if (assert) assertNumArgs(arguments.length, 2, "Logic.HalfAdderCarry"); this.a = formula1; this.b = formula2; }; Logic._defineFormula(Logic.HalfAdderCarry, 'hcarry', { generateClauses: function (isTrue, t) { return t.generate(isTrue, Logic.and(this.a, this.b)); } }); Logic.FullAdderSum = function (formula1, formula2, formula3) { if (assert) assert(formula1, isFormulaOrTerm); if (assert) assert(formula2, isFormulaOrTerm); if (assert) assert(formula3, isFormulaOrTerm); if (assert) assertNumArgs(arguments.length, 3, "Logic.FullAdderSum"); this.a = formula1; this.b = formula2; this.c = formula3; }; Logic._defineFormula(Logic.FullAdderSum, 'fsum', { generateClauses: function (isTrue, t) { return t.generate(isTrue, Logic.xor(this.a, this.b, this.c)); } }); Logic.FullAdderCarry = function (formula1, formula2, formula3) { if (assert) assert(formula1, isFormulaOrTerm); if (assert) assert(formula2, isFormulaOrTerm); if (assert) assert(formula3, isFormulaOrTerm); if (assert) assertNumArgs(arguments.length, 3, "Logic.FullAdderCarry"); this.a = formula1; this.b = formula2; this.c = formula3; }; Logic._defineFormula(Logic.FullAdderCarry, 'fcarry', { generateClauses: function (isTrue, t) { return t.generate(! isTrue, Logic.atMostOne(this.a, this.b, this.c)); } }); // Implements the Adder strategy from the MiniSat+ paper: // http://minisat.se/downloads/MiniSat+.pdf // "Translating Pseudo-boolean Constraints into SAT" // // Takes a list of list of Formulas. The first list is bits // to give weight 1; the second is bits to give weight 2; // the third is bits to give weight 4; and so on. // // Returns an array of Logic.FormulaOrTerm. var binaryWeightedSum = function (varsByWeight) { if (assert) assert(varsByWeight, isArrayWhere(isArrayWhere(isFormulaOrTerm))); // initialize buckets to a two-level clone of varsByWeight var buckets = _.map(varsByWeight, _.clone); var lowestWeight = 0; // index of the first non-empty array var output = []; while (lowestWeight < buckets.length) { var bucket = buckets[lowestWeight]; if (! bucket.length) { output.push(Logic.FALSE); lowestWeight++; } else if (bucket.length === 1) { output.push(bucket[0]); lowestWeight++; } else if (bucket.length === 2) { var sum = new Logic.HalfAdderSum(bucket[0], bucket[1]); var carry = new Logic.HalfAdderCarry(bucket[0], bucket[1]); bucket.length = 0; bucket.push(sum); pushToNth(buckets, lowestWeight+1, carry); } else { // Whether we take variables from the start or end of the // bucket (i.e. `pop` or `shift`) determines the shape of the tree. // Empirically, some logic problems are faster with `shift` (2x or so), // but `pop` gives an order-of-magnitude speed-up on the Meteor Version // Solver "benchmark-tests" suite (Slava's benchmarks based on data from // Rails). So, `pop` it is. var c = bucket.pop(); var b = bucket.pop(); var a = bucket.pop(); var sum = new Logic.FullAdderSum(a, b, c); var carry = new Logic.FullAdderCarry(a, b, c); bucket.push(sum); pushToNth(buckets, lowestWeight+1, carry); } } return output; }; // Push `newItem` onto the array at arrayOfArrays[n], // first ensuring that it exists by pushing empty // arrays onto arrayOfArrays. var pushToNth = function (arrayOfArrays, n, newItem) { while (n >= arrayOfArrays.length) { arrayOfArrays.push([]); } arrayOfArrays[n].push(newItem); }; var checkWeightedSumArgs = function (formulas, weights) { if (assert) assert(formulas, isArrayWhere(isFormulaOrTerm)); if (typeof weights === 'number') { if (assert) assert(weights, Logic.isWholeNumber); } else { if (assert) assert(weights, isArrayWhere(Logic.isWholeNumber)); if (formulas.length !== weights.length) { throw new Error("Formula array and weight array must be same length" + "; they are " + formulas.length + " and " + weights.length); } } }; Logic.weightedSum = function (formulas, weights) { checkWeightedSumArgs(formulas, weights); if (formulas.length === 0) { return new Logic.Bits([]); } if (typeof weights === 'number') { weights = _.map(formulas, function () { return weights; }); } var binaryWeighted = []; _.each(formulas, function (f, i) { var w = weights[i]; var whichBit = 0; while (w) { if (w & 1) { pushToNth(binaryWeighted, whichBit, f); } w >>>= 1; whichBit++; } }); return new Logic.Bits(binaryWeightedSum(binaryWeighted)); }; Logic.sum = function (/*formulaOrBitsOrArray, ...*/) { var things = _.flatten(arguments); if (assert) assert(things, isArrayWhere(isFormulaOrTermOrBits)); var binaryWeighted = []; _.each(things, function (x) { if (x instanceof Logic.Bits) { _.each(x.bits, function (b, i) { pushToNth(binaryWeighted, i, b); }); } else { pushToNth(binaryWeighted, 0, x); } }); return new Logic.Bits(binaryWeightedSum(binaryWeighted)); }; //////////////////////////////////////// Logic.Solver.prototype.solve = function (_assumpVar) { var self = this; if (_assumpVar !== undefined) { if (! (_assumpVar >= 1)) { throw new Error("_assumpVar must be a variable number"); } } if (self._unsat) { return null; } while (self._numClausesAddedToMiniSat < self.clauses.length) { var i = self._numClausesAddedToMiniSat; var terms = self.clauses[i].terms; if (assert) assert(terms, isArrayWhere(Logic.isNumTerm)); var stillSat = self._minisat.addClause(terms); self._numClausesAddedToMiniSat++; if (! stillSat) { self._unsat = true; return null; } } if (assert) assert(this._num2name.length - 1, Logic.isWholeNumber); self._minisat.ensureVar(this._num2name.length - 1); var stillSat = (_assumpVar ? self._minisat.solveAssuming(_assumpVar) : self._minisat.solve()); if (! stillSat) { if (! _assumpVar) { self._unsat = true; } return null; } return new Logic.Solution(self, self._minisat.getSolution()); }; Logic.Solver.prototype.solveAssuming = function (formula) { if (assert) assert(formula, isFormulaOrTerm); // Wrap the formula in a formula of type Assumption, so that // we always generate a var like `$assump123`, regardless // of whether `formula` is a Term, a NotFormula, an already // required or forbidden Formula, etc. var assump = new Logic.Assumption(formula); var assumpVar = this._formulaToTerm(assump); if (! (typeof assumpVar === 'number' && assumpVar > 0)) { throw new Error("Assertion failure: not a positive numeric term"); } // Generate clauses as if we used the assumption variable in a // clause, in the positive. So if we assume "A v B", we might get a // clause like "A v B v -$assump123" (or actually, "$or1 v // -$assump123"), as if we had used $assump123 in a clause. Instead // of using it in a clause, though, we temporarily assume it to be // true. this._useFormulaTerm(assumpVar); var result = this.solve(assumpVar); // Tell MiniSat that we will never use assumpVar again. // The formula may be used again, however. (For example, you // can solve assuming a formula F, and if it works, require F.) this._minisat.retireVar(assumpVar); return result; }; Logic.Assumption = func