UNPKG

fdjs

Version:

Finite Domain Constrain Solver

825 lines (747 loc) 29.8 kB
// Copyright (c) 2011, Srikumar K. S. (srikumarks@gmail.com) // License: New BSD (http://www.opensource.org/licenses/bsd-license.php) // // Module: FD // Status: Work (very much) in progress // // Basic finite domain constraint programming using the // "computation spaces" idea in Oz to factorize a search // problem into propagators, distributors, search strategies // and problem scripts. // // Exports: // // Class FD.space is a computation space. // FD.space has methods that add propagators to spaces. // Each space has a "brancher" object that can branch // a space if it isn't solved yet and some options can be // explored. You can enqueue different branchers into a // space's brancher object which will then control how the // search tree is created. // // Namespace FD.distribute has the following strategies - // FD.distribute.naive // FD.distribute.fail_first // // Namespace FD.search has the following search algo - // FD.search.depth_first // // NOTE: The fd.js module needs to be loaded prior to // loading this file. try { FD.space; } catch (e) { alert("fd.js module needs to be loaded before fd-tests.js"); } (function () { // A simple script for solving sudoku puzzles. // 'board' is an object whose keys are sudoku // cell references of the form <letter><digit> // where <letter> gives the row letter from // ABCDEFGHI and <digit> gives column from // 123456789. The solution of the space will be // an object with a key for each cell. function sudoku(board) { return function (S) { var rows = ['A','B','C','D','E','F','G','H','I']; var cols = ['1','2','3','4','5','6','7','8','9']; var root = []; var i, j, k, i2, j2; // Declare board places. for (i = 0; i < 9; ++i) { for (j = 0; j < 9; ++j) { root.push(rows[i] + cols[j]); } } S.decl(root, [[1,9]]); // Add row constraints. for (i = 0; i < 9; ++i) { k = []; for (j = 0; j < 9; ++j) { k.push(rows[i] + cols[j]); } S.distinct(k); } // Add column constraints for (i = 0; i < 9; ++i) { k = []; for (j = 0; j < 9; ++j) { k.push(rows[j] + cols[i]); } S.distinct(k); } // Add box constraints. for (i = 0; i < 3; ++i) { for (j = 0; j < 3; ++j) { k = []; for (i2 = 0; i2 < 3; ++i2) { for (j2 = 0; j2 < 3; ++j2) { k.push(rows[i * 3 + i2] + cols[j * 3 + j2]); } } S.distinct(k); } } // Initialize the board. for (i in board) { S.num(i, board[i]); } // Distribution strategy is fail first, since that is // likely to cause maximum propagation for this puzzle. FD.distribute.fail_first(S, root); return S; }; } // Helps check that all the elements of the array are different // from each other. function verify_distinct(values) { var arr, i, j, k, N; if (values instanceof Array) { arr = values; } else if (values instanceof Object) { arr = []; for (k in values) { arr.push(values[k]); } } else { throw "verify_distinct: Invalid argument."; } for (i = 0, N = arr.length; i < N; ++i) { for (j = 0; j < i; ++j) { if (arr[i] === arr[j]) { return false; } } } return true; } // Checks that the given board is solved. function verify_sudoku(board) { var rows = ['A','B','C','D','E','F','G','H','I']; var cols = ['1','2','3','4','5','6','7','8','9']; var i, j, k, i2, j2, b; // Add row constraints. for (i = 0; i < 9; ++i) { k = []; for (j = 0; j < 9; ++j) { b = board[rows[i] + cols[j]]; if (b < 1 || b > 9) { // Check domain validity. return false; } k.push(b); } if (!verify_distinct(k)) { return false; } } // Add column constraints for (i = 0; i < 9; ++i) { k = []; for (j = 0; j < 9; ++j) { k.push(board[rows[j] + cols[i]]); } if (!verify_distinct(k)) { return false; } } // Add box constraints. for (i = 0; i < 3; ++i) { for (j = 0; j < 3; ++j) { k = []; for (i2 = 0; i2 < 3; ++i2) { for (j2 = 0; j2 < 3; ++j2) { k.push(board[rows[i * 3 + i2] + cols[j * 3 + j2]]); } } if (!verify_distinct(k)) { return false; } } } return true; } // Some simple tests. var tests = [ { name: 'test_plus', description: 'Simple plus test', search: FD.search.depth_first, verify: function (sol) { return sol.Y === 7; }, script: function (S) { return S.num('X', 3).num('Z', 10).decl('Y').plus('X', 'Y', 'Z'); } }, { name: 'test_plus_nosol', description: "plus, but no solution exists", search: FD.search.depth_first, verify: function (sol) { return false; }, script: function (S) { // In this case no solution exists. return S.num('X', 13).num('Z', 10).decl('Y').plus('X', 'Y', 'Z'); } }, { name: 'test_plus_range', description: "Domain narrowing by propagators", search: FD.search.depth_first, verify: function (sol) { return true; // TODO: CHEATING! }, script: function (S) { // No *unique* solution exists, but Y will be narrowed to a range. // TODO: This test works, but is not printed out currently since the // solution is not unique. return S.num('X', 13).decl('Z', [[0, 100]]).decl('Y').plus('X', 'Y', 'Z'); } }, { name: 'test_sum', description: "Simple 'sum' test", search: FD.search.depth_first, verify: function (sol) { return true; // TODO: CHEATING! }, script: function (S) { // A mixture of some constraints X + Y + Z = W and X < Y. // TODO: This test works, but is not printed out currently since the // solution is not unique. return S.decl(['X', 'Y', 'Z'], [[0, 10]]).decl('W').sum(['X', 'Y', 'Z'], 'W').lt('X', 'Y'); } }, { name: 'test_naive', description: "Simple test of naive distribution", search: FD.search.depth_first, verify: function (sol) { return sol.X + sol.Y === sol.Z; }, script: function (S) { // List out all possible solutions of X + Y = Z with some domain limits. S.decl('X', [[0, 3]]); S.decl('Y', [[4, 6]]); S.decl('Z'); S.plus('X', 'Y', 'Z'); FD.distribute.naive(S, ['X', 'Y']); return S; } }, { name: 'test_wsum', description: "Simple test of wsum with fail_first distribution", search: FD.search.depth_first, verify: function (sol) { return 10 * sol.A + sol.B === sol.C; }, script: function (S) { // Test of "weighted sum" and enumeration of all // solutions using the "fail first" distribution // strategy. S.decl(['A','B','C']); S.wsum([10,1], ['A','B'], 'C'); S.vars.C.set_dom([[0,20]]); FD.distribute.fail_first(S, ['A','B','C']); return S; } }, { name: 'test_distinct', description: "Simple distinct test", search: FD.search.depth_first, verify: function (sol) { return (sol.A + sol.B === sol.C) && (sol.A !== sol.B); }, script: function (S) { // Test of "distinct" constraint. var root = ['A','B','C']; S.decl(root); S.decl(['A', 'B'], [[0, 10]]); S.distinct(['A','B']); S.plus('A','B','C'); FD.distribute.fail_first(S, root); return S; } }, { name: 'test_simple_multiple_branchers', description: "Simple test of multiple branching strategies mixed in.", search: FD.search.depth_first, verify: function (sol) { return sol.A !== sol.B; }, script: function (S) { S.decl('A', [[1,5]]); S.decl('B', [[1,5]]); S.neq('A', 'B'); FD.distribute.split(S, 'A'); FD.distribute.naive(S, 'B'); return S; } }, { name: 'test_send_more_money', description: "The famous SEND + MORE = MONEY problem, using fail_first distribution.", search: FD.search.depth_first, verify: function (sol) { var SEND = sol.S * 1000 + sol.E * 100 + sol.N * 10 + sol.D; var MORE = sol.M * 1000 + sol.O * 100 + sol.R * 10 + sol.E; var MONEY = sol.M * 10000 + sol.O * 1000 + sol.N * 100 + sol.E * 10 + sol.Y; return SEND + MORE === MONEY; }, script: function (S) { // The famous "SEND + MORE = MONEY" problem. // TODO: Note that though this works, it doesn't // produce the same search tree that Oz does. // Either I don't have as strong propagators as Oz // just yet or the way wsum groups its operations // needs to be changed. Seems enough to correctly // solve the problem though. var root = ['S','E','N','D','M','O','R','Y']; S.decl(root, [[0,9]]).distinct(root); S.vars.S.set_dom([[1,9]]); S.vars.M.set_dom([[1, 9]]); var t1 = S.temp(); var t2 = S.temp(); var t3 = S.temp(); S.wsum([1000,100,10,1], ['S','E','N','D'], t1); S.wsum([1000,100,10,1], ['M','O','R','E'], t2); S.wsum([10000,1000,100,10,1], ['M','O','N','E','Y'], t3); S.plus(t1, t2, t3); FD.distribute.fail_first(S, root); return S; } }, { name: 'test_send_more_money_fn', description: "Same SEND + MORE = MONEY in functional notation", search: FD.search.depth_first, verify: function (sol) { var SEND = sol.S * 1000 + sol.E * 100 + sol.N * 10 + sol.D; var MORE = sol.M * 1000 + sol.O * 100 + sol.R * 10 + sol.E; var MONEY = sol.M * 10000 + sol.O * 1000 + sol.N * 100 + sol.E * 10 + sol.Y; return SEND + MORE === MONEY; }, script: function (S) { // Same problem as above, but expressed in "functional" notation. var root = ['S','E','N','D','M','O','R','Y']; S.decl(root, [[0,9]]).distinct(root); S.decl(['S','M'], [[1,9]]); S.plus(S.wsum([1000,100,10,1], ['S','E','N','D']), S.wsum([1000,100,10,1], ['M','O','R','E']), S.wsum([10000,1000,100,10,1], ['M','O','N','E','Y'])); FD.distribute.fail_first(S, root); return S; } }, { name: 'test_einstein', description: 'The famous <a href="http://www.stanford.edu/~laurik/fsmbook/examples/Einstein\'sPuzzle.html">Einstein puzzle</a>', search: FD.search.depth_first, verify: function (sol) { var root = [ ['Englishman', 'Swede', 'Dane', 'Norwegian', 'German'], ['Cats', 'Dogs', 'Birds', 'Horses', 'Fish'], ['Bier', 'Coffee', 'Tea', 'Milk', 'Water'], ['PallMall', 'Dunhills', 'Blend', 'BlueMasters', 'Prince'], ['BlueHouse', 'RedHouse', 'GreenHouse', 'WhiteHouse', 'YellowHouse'] ]; function are_distinct(arr) { return verify_distinct(arr.map(function (name) { return sol[name]; })); } root[0].forEach(function (man) { if (sol[man] === sol.Fish) { display.show(man + ' keeps Fish'); } }); return (root.map(are_distinct).reduce(function (acc, b) { return acc && b; }, true) && (sol.Englishman === sol.RedHouse) && (sol.Swede === sol.Dogs) && (sol.Dane === sol.Tea) && (sol.WhiteHouse === sol.GreenHouse + 1) && (sol.PallMall === sol.Birds) && (sol.Milk === 3) && (sol.Norwegian === 1) && (sol.Blend + 1 === sol.Cats || sol.Cats + 1 === sol.Blend) && (sol.BlueMasters === sol.Bier) && (sol.Horses + 1 === sol.Dunhills || sol.Dunhills + 1 === sol.Horses) && (sol.German === sol.Prince) && (sol.Norwegian + 1 === sol.BlueHouse || sol.BlueHouse + 1 === sol.Norwegian) && (sol.Blend + 1 === sol.Water || sol.Water + 1 === sol.Blend)); }, script: function (S) { var root = [ ['Englishman', 'Swede', 'Dane', 'Norwegian', 'German'], ['Cats', 'Dogs', 'Birds', 'Horses', 'Fish'], ['Bier', 'Coffee', 'Tea', 'Milk', 'Water'], ['PallMall', 'Dunhills', 'Blend', 'BlueMasters', 'Prince'], ['BlueHouse', 'RedHouse', 'GreenHouse', 'WhiteHouse', 'YellowHouse'] ]; var allvars = Array.prototype.concat.apply([], root); S.decl(allvars, [[1,5]]); root.forEach(function (varset) { S.distinct(varset); }); var one = S.konst(1); function nextTo(a,b) { // a nextTo b is modeled as // a + 1 == b or b + 1 == a // Reifying both conditions to boolean values C1 and C2, the "or" // can be expressed as C1 + C2 == 1 return S.plus(S.reified('eq', [S.plus(b, one), a]), S.reified('eq', [S.plus(a, one), b]), one); } S.eq('Englishman', 'RedHouse'); S.eq('Swede', 'Dogs'); S.eq('Dane', 'Tea'); S.eq('WhiteHouse', S.plus('GreenHouse', one)); S.eq('GreenHouse', 'Coffee'); S.eq('PallMall', 'Birds'); S.eq('YellowHouse', 'Dunhills'); S.eq('Milk', S.konst(3)); S.eq('Norwegian', one); nextTo('Blend', 'Cats'); S.eq('BlueMasters', 'Bier'); nextTo('Horses', 'Dunhills'); S.eq('German', 'Prince'); nextTo('Norwegian', 'BlueHouse'); nextTo('Blend', 'Water'); FD.distribute.fail_first(S, allvars); return S; } }, { name: 'test_sudoku', description: "A <a href=\"http://en.wikipedia.org/wiki/Sudoku\">simple</a> Sudoku board", search: FD.search.depth_first, verify: verify_sudoku, script: function (S) { return sudoku({ A5:8, A8:7, A9:9, B4:4, B5:1, B6:9, B9:5, C2:6, C7:2, C8:8, D1:7, D5:2, D9:6, E1:4, E4:8, E6:3, E9:1, F1:8, F5:6, F9:3, G2:9, G3:8, G8:6, H1:6, H4:1, H5:9, H6:5, I1:5, I2:3, I5:7 })(S); } }, { name: 'test_extreme_sudoku', description: "An <a href=\"http://www.extremesudoku.info/sudoku.html\">extreme</a> sudoku puzzle", search: FD.search.depth_first, verify: verify_sudoku, script: function (S) { return sudoku({ A1:4, A4:6, A7:7, B2:7, B5:4, B8:2, C3:5, C6:1, C9:6, D3:3, D6:5, D9:8, E2:2, E5:1, E8:9, F1:9, F4:7, F7:4, G1:3, G4:1, G7:9, H2:1, H5:8, H8:6, I3:7, I6:6, I9:1 })(S); } }, { name: 'test_really_hard_sudoku', description: "A <a href=\"http://4puz.com/hard_puzzle_v1p1.html\">really hard</a> sudoku", search: FD.search.depth_first, verify: verify_sudoku, script: function (S) { return sudoku({ A6:8, A7:5, B2:2, B6:6, B9:1, C2:3, C3:9, C8:4, C9:2, D7:6, D8:1, E1:4, E9:5, F2:1, F3:7, G1:2, G2:5, G7:1, G8:9, H1:3, H4:4, H8:2, I3:8, I4:9 })(S); } }, { name: 'test_norvig_hard_sudoku', description: "A <a href=\"http://norvig.com/sudoku.html\">Norvig hard</a> sudoku puzzle. There are multiple solutions but it still doesn't take very long find quite a few of them.", search: FD.search.depth_first, verify: verify_sudoku, max_solutions: 10, script: function (S) { // This puzzle blows Chrome if you try to find all solutions! // So we don't do that and limit the number of solutions to find. return sudoku({ C4:3, C5:2, C6:5, C9:6, D3:6, D6:3, D8:5, D9:4, E3:3, F2:4, F3:5, G1:2, G6:8, H2:5, H3:9, H9:8, I6:6 })(S); } }, { name: 'test_worlds_hardest_sudoku', description: "The <a href=\"http://zonkedyak.blogspot.com/2006/11/worlds-hardest-sudoku-puzzle-al.html\">world's hardest (yeah right!)</a> sudoku puzzle.", search: FD.search.depth_first, verify: verify_sudoku, script: function (S) { return sudoku({ A3:7, A7:3, B2:4, B9:7, C1:3, C8:1, D1:6, D6:4, E2:1, E5:8, E9:2, F3:5, F4:3, F7:9, G3:9, G4:6, G7:5, H2:3, H5:2, H9:8, I1:1, I6:7, I8:9 })(S); } }, { name: 'test_fiendishly_hard_sudoku', description: "The <a href=\"http://hagaregn.org.uk/npsudoku/sudoku.html\">fiendishly hard (yeah right!)</a> sudoku puzzle.", search: FD.search.depth_first, verify: verify_sudoku, script: function (S) { return sudoku({ A1:7, B2:3, B4:5, C5:4, C7:2, D4:3, D8:7, E1:2, E7:6, F2:5, F4:1, F6:8, G5:7, H1:6, H5:2, I8:3, I9:1 })(S); } }, { name: 'test_blank_sudoku', description: "Starting from a blank slate, find one solution", search: FD.search.depth_first, verify: verify_sudoku, max_solutions: 1, script: sudoku({}) }, { name: 'test_reified_lt', description: "Simple test of reified less-than operator: X < Y :: Z, Z == 0", search: FD.search.depth_first, verify: function (sol) { return (sol.X < sol.Y && (sol.Z === 1)) || (sol.X >= sol.Y && (sol.Z === 0)); }, script: function (S) { S.decl('X', [[1, 10]]).decl('Y', [[5, 6]]).decl('Z', [[0,0]]); S.reified('lt', ['X', 'Y'], 'Z'); FD.distribute.naive(S, ['X', 'Y', 'Z']); return S; } }]; function test_simple_bab(dist_strategy) { return { name: 'test_simple_bab+'+dist_strategy, description: ("Simple bab X + Y = Z, maximize Z, with X `neq` A and X, Y, A in [[1,5]] " + "(A is irrelevant to the optimization). Note that A != X condition is not verified automatically. (Lazy!)"), single_step: true, verify: function (sol) { return sol.X + sol.Y === sol.Z; }, search: (function () { function ordering(S, solution) { S.gt('Z', S.konst(solution.Z)); } return function (state) { state.single_step = this.single_step; state.is_solved = state.is_solved || FD.search.solve_for_variables(['X', 'Y']); return FD.search.branch_and_bound(state, ordering); }; })(), script: function (S) { S.decl(['X', 'Y', 'A'], [[1, 5]]).decl('Z'); S.plus('X', 'Y', 'Z'); S.neq('X', 'A'); FD.distribute[dist_strategy](S, ['X', 'Y']); return S; } }; } function test_mozart_photo_bab(dist_strategy) { return { name: 'test_mozart_photo_bab+'+dist_strategy, description: 'From <a href="http://www.mozart-oz.org/documentation/fdt/node44.html#section.bab.align">Mozart/Oz BAB example</a>.', single_step: true, verify: function (sol) { var sat = sol.Satisfaction; delete sol.Satisfaction; // Don't include the 'Satisfaction' value in the distinct test. return verify_distinct(sol) && (function () { var count = 0; var preferences = [ ['Betty', 'Gary'], ['Betty', 'Mary'], ['Chris', 'Betty'], ['Chris', 'Gary'], ['Fred', 'Mary'], ['Fred', 'Donald'], ['Paul', 'Fred'], ['Paul', 'Donald'] ]; preferences.forEach(function (p) { if (Math.abs(sol[p[0]] - sol[p[1]]) < 1.5) { count++; } }); return Math.abs(count - sat) < 0.1; }()); }, search: (function () { function ordering(S, solution) { S.gt('Satisfaction', S.konst(solution.Satisfaction)); } return function (state) { state.single_step = this.single_step; state.is_solved = state.is_solved || FD.search.solve_for_variables([ 'Betty', 'Chris', 'Donald', 'Fred', 'Gary', 'Mary', 'Paul' ]); // state.is_solved = FD.search.solve_for_propagators; return FD.search.branch_and_bound(state, ordering); }; })(), script: function (S) { var one = S.konst(1); var zero = S.konst(0); function satisfied(p) { var a = S.reified('eq', [S.plus(p[0], one), p[1]]); var b = S.reified('eq', [S.plus(p[1], one), p[0]]); return S.plus(a, b); } var persons = ['Betty', 'Chris', 'Donald', 'Fred', 'Gary', 'Mary', 'Paul']; var preferences = [ ['Betty', 'Gary'], ['Betty', 'Mary'], ['Chris', 'Betty'], ['Chris', 'Gary'], ['Fred', 'Mary'], ['Fred', 'Donald'], ['Paul', 'Fred'], ['Paul', 'Donald'] ]; S.decl(persons, [[1, persons.length]]); S.decl('Satisfaction'); S.distinct(persons); S.sum(preferences.map(satisfied), 'Satisfaction'); S.lt('Fred', 'Betty'); FD.distribute[dist_strategy](S, persons); return S; } }; } tests.push(test_simple_bab('naive')); tests.push(test_simple_bab('fail_first')); tests.push(test_simple_bab('split')); tests.push(test_mozart_photo_bab('naive')); tests.push(test_mozart_photo_bab('fail_first')); if (false) { var t = test_mozart_photo_bab('naive'); t.search = FD.search.depth_first; t.name = t.name + '+depth_first'; tests.push(t); } tests.push(test_mozart_photo_bab('split')); function test_n_queens(N, max_solutions) { return { name: 'test_'+N+'_queens', description: ''+N+' queens problem', search: FD.search.depth_first, max_solutions: max_solutions, verify: function (sol) { var i, j; if (!verify_distinct(sol)) { return false; } for (i = 1; i <= N; ++i) { for (j = 1; j < i; ++j) { if (sol['R'+i]-i === sol['R'+j]-j || sol['R'+i]+i === sol['R'+j]+j) { return false; } } } return true; }, script: function (S) { var i, j, k; var root = []; for (i = 1; i <= N; ++i) { root.push('R'+i); } S.decl(root, [[1, N]]); S.distinct(root); for (i = 0; i < N; ++i) { for (j = 0; j < i; ++j) { k = S.konst(i-j); S.neq(S.plus(root[j], k), root[i]); S.neq(S.plus(root[i], k), root[j]); } } FD.distribute.fail_first(S, root); return S; } }; } tests.push(test_n_queens(8)); var run_tests = (function () { var display = {}; display.__proto__ = (document || console); display.show = ((document && document.write) ? (function () { return document.write.apply(document, arguments); }) : (function () { return console.log.apply(console, arguments); })); this.display = display; var strings = { correct: '<span style="color:green">CORRECT</span>', wrong: '<span style="color:red"><b>WRONG!</b></span>' }; function show_result(test, result, start) { if (result.status === 'solved' || (!result.space.failed && result.space.brancher.queue.length === 0)) { var end = Date.now(); var S = result.best || result.space; var failure = (result.is_solved(S) && test.verify) ? strings[test.verify(S.solution()) ? 'correct' : 'wrong'] : ''; display.show('<p><tt>' + '['+(end-start)+' ms] ' + failure + ' ' + result.status + ': ' + JSON.stringify(S.solution()) + '</tt></p>'); return 1; } else { return 0; } } return function () { var tests_copy = tests.slice(0); function dotest(test) { if (test.do_not_run || !test.script) { return; } // Default to depth_first search if unspecified. test.search = test.search || FD.search.depth_first; var S = test.script(new FD.space()); if (S) { var start = Date.now(); var state = {space: S}; // for-debugging: {space: S, verify: test.verify}; var count = 0; display.show('<p><b><a name="' + test.name + '">' + test.name + '</a>:</b> ' + test.description + '</p>' + (test.single_step ? ' (single stepping)' : '')); do { state = test.search(state); count += show_result(test, state, start); if (count >= test.max_solutions) { display.show('<p>--- reached given limit of ' + test.max_solutions + ' solutions ---</p>'); break; } } while (state.more); if (count === 0) { display.show('<p><tt>No solution</tt></p>'); } display.show('<p>succ = ' + S.succeeded_children + ', fail = ' + S.failed_children + ', stab = ' + S.stable_children + ', <b>total</b> = ' + (S.succeeded_children + S.failed_children + S.stable_children) + '</p>'); display.show('<hr/>'); } } function dotests() { if (tests_copy.length > 0) { dotest(tests_copy.shift()); window.scrollTo(0,document.body.scrollHeight); setTimeout(dotests, 0); } } dotests(); }; })(); // Comment this out if you don't want to run the tests. run_tests(); })();