{
  "id": 200539,
  "title": "Notebook Prize write-up: Annealing + SAT to 0.035",
  "url": "/competitions/conways-reverse-game-of-life-2020/discussion/200539",
  "author_name": "",
  "post_date": "2020-12-01T00:37:45.685355200Z",
  "votes": 8,
  "comment_count": 1,
  "views": 0,
  "content": "<h1>Intro</h1>\n<p>Welcome!<br>\nI'd like to present a solution, that gives a score 0.035 after just 9 hours of computation and goes to 0.015 after couple of weeks.<br>\nShortly, there was used a combination of SAT, greedy gradient optimization (annealing) and  hardcore optimizations.<br>\nFor developing was chosen C++ language as it seems a task that is critical for performance and every small advantage can be beneficial.</p>\n<p><em>for who'd like to read code rather than text:</em><br>\nNotebook with final submission (binaries): <a href=\"https://www.kaggle.com/knstqq/knst-conways-rt\" target=\"_blank\">https://www.kaggle.com/knstqq/knst-conways-rt</a><br>\nCommented version with source code: <a href=\"https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\" target=\"_blank\">https://www.kaggle.com/knstqq/conway-notebook-annealing-sat</a><br>\nGitHub with more details and tools: (will be added soon)</p>\n<h1>Phase 1. Simplicity</h1>\n<p>Let's take input and put it as output. ~0.17 is here. It is not too bad score, good baseline to start.<br>\nSome boards become periodic, so, choose delta+1, delta+2, … position and look to score. Tiny improvement: ~0.145<br>\nTrying more position: random fields, empty… Tiny improvement again: ~0.138</p>\n<h1>Phase 2. Greediness</h1>\n<p>Instead of generating completely new board, better to try to improve current one step-by-step. For the \"best\" position get by Phase 1 the each cell is changed one-by-one. If score is improved, changed is preserved, otherwise return previous state. To repeat it millions of times… ~0.105<br>\nLet's do it smarter. If score is not actually changed we preserve some changed values if it does not become worse. It will help to achieve position that need more than 1 bit to change without actually changing position to become worse. ~0.075 achieved.<br>\nThis solution is running on Ryzen 3900 and after long computations the score is improving, but very slowly… Good progress at the moment, but I need more!<br>\nTo avoid be stucked in local maximums I added some random noise after thousands of greedy iterations. Score became 0.068.</p>\n<h1>Phase 3. Stimulants</h1>\n<p>That works nice, but it is slower and slower with every hour, every day and at some moment it started to be that slow, that heating of my CPU lost attractiveness.<br>\nTime to put stimulants to solution.<br>\nIn implementation, the field is presented as linear array of N*N elements. And every time, when just a single position is changed, the score is heavily re-calculated.</p>\n<pre><code>const int dx8[] = {-1, -1, -1,  0,  0,  1,  1,  1};\nconst int dy8[] = {-1,  0,  1, -1,  1, -1,  0,  1};\nbool calc(const vector&lt;bool&gt;&amp; field, int pos) {\n    int x = pos % n;\n    int y = pos / n;\n    int count = 0;\n    for (int i = 0; i &lt; 8; ++i) {\n        int a = x + dx8[i];\n        int b = y + dy8[i];\n        while (a &lt; 0) a += n;\n        while (a &gt;= n) a -= n;\n        while (b &lt; 0) b += n;\n        while (b &gt;= n) b -= n;\n\n        if (field[b * n + a]) ++count;\n    }\n    if (count &lt; 2) return false;\n    if (count &gt; 3) return false;\n    if (field[pos]) return true;\n    if (count == 3) return true;\n    return false;\n}\nvector&lt;bool&gt; run_step(vector&lt;bool&gt; start) {\n    vector&lt;bool&gt; next(N, false);\n    for (int i = 0; i &lt; N; ++i) {\n        next[i] = calc(start, i);\n    }\n}\n</code></pre>\n<p>For sure, profiler shows that <code>run_step</code> and <code>calc</code> together are consuming 99% of CPU time.<br>\nAfter lots of runs benchmark, profiler (perf) and looking to generated asm code, this simple solution evolved and speeded up in 100x times:</p>\n<ul>\n<li>specified aggressive optimization options: <code>-O3 -march=native -funroll-loops -fno-omit-frame-pointer</code> (10% faster)</li>\n<li><code>vector&lt;bool&gt;</code> is too inefficient in C++, the replacement to <code>vector&lt;char&gt;</code> with 1 field in 1 byte works much faster. Even it consume more memory, but the asm code is much more efficient (20% faster)</li>\n<li>used <code>unsigned ints</code> let GCC to generate more efficient code rather than signed int due to more efficient asm code again (20% faster)</li>\n<li>used simple conditions <code>return (count | field[pos]) == 3</code> instead of 4 condition if-else (5% faster)</li>\n<li>specialized implementation for cells in the middle of board - because no need to check \"is it border\" heavily (+200% faster)</li>\n<li>for borders were implemented specialized optimized methods: one for left side, one for right side, for top side, for down side (+100% faster)</li>\n<li>implemented partial updates for only square with side <code>(2delta + 1)</code> cells (+1000% faster)</li>\n<li>instead of returning to previous position by re-calculation saving all changes to changelog and use it only if returning is needed (+5% faster)</li>\n</ul>\n<p>I stopped to write down achieved score here… So, whatever exact number is, but greediness becomes much faster. ~0.05 ?</p>\n<h1>Phase 4. Perfectness.</h1>\n<p>Until now perfect solution have been found very inefficient even for delta=1, just some good estimation.<br>\nTime to look for SAT.<br>\nThe winner of SAT Competition 2020 is KISSAT. There is no any other option for me rather then best sat solver at the moment.<br>\nIndeed, as checked someday later Z3 works well, but worse. Pycosat is much slower. Some other solver (I will not call their names) are much slower, some even just crashes with no reason.</p>\n<p>Each task is converted to CNF with hundreds of thousands of conjunctions. They are minimized sufficiently by sympy something like that:</p>\n<pre><code>$ python3\n&gt;&gt;&gt; from sympy import *\n&gt;&gt;&gt; x, y, z = symbols('x y z')\n&gt;&gt;&gt; simplify_logic((x &amp; y &amp; ~z) | (x &amp; ~y &amp; z), form=\"cnf\")\nx &amp; (y | z) &amp; (~y | ~z)\n</code></pre>\n<p>You just need to wait an hour until sympy will finish my rigorous conjunction (single-thread).</p>\n<p>Serialization throught files DIMACS is too slow (10 Mb CNF to deserialize/parse back), so, necessary objects are extract for kissat solver and staticly linked to my solution, called directly by C interfaces.<br>\nThat give me all solved delta=1 in less than 5 minutes (24 cores with eaten just 4Gb RAM).<br>\n99% of delta 2 is solved in 4 hours, and fully solved in 36 hours.<br>\nSome minor changes (added time-outs, pre-selecting tasks with low weight, with big empty space, etc) let to solve 50% of delta=3, 30% of delta=4, 20% of delta=5<br>\nScore is 0.03</p>\n<h1>Phase 5. Avidity</h1>\n<p>A beeing greedy is good in short-term, but some times you need to give away things… Don't be too greedy. Heheh.<br>\nLet score in greedy methods sometime drop down from maximum, to 1-2-3-4-5 lost poings… After some tuning and experiments I re-invent Annealing for optimization.<br>\nJump to 0.015 in less than 1 day of computation.</p>\n<h1>Phase 6. Revealing.</h1>\n<p>Until this moment I did not used sat more, but as was written in duscussion, lots of delta=3, 4, 5 can be solved.<br>\nAfter carefully chosed team members (and lots because their achievements) we jumped to 0.07: annealing for good aproximation is not good enough without \"perfect\" solutions.<br>\nScore: <strong>under 0.0064</strong> at the final (offline solution)</p>\n<h1>Phase 7. Rebirth.</h1>\n<p>In last day all code was re-written a lot of times, all params carefully adjusted. More than 100 tests on subset of data is done to find the best combination for real-time notebook solution<br>\nThanks <a href=\"https://www.kaggle.com/robertc\" target=\"_blank\">@robertc</a> for an advice to use \"checked\" position as start one helped significantly.<br>\nHere is it, *<em>the score 0.035</em> for notebook*.</p>\n<h1>Bonus: Dead Ends</h1>\n<p>Approaches that didn't worked well:</p>\n<ul>\n<li>partial SAT (specify not all fields, but only some to speed up SAT solver). It gives too bad approximation that worse than greedy solutions.</li>\n<li>step SAT (calculate for delta=3 as delta=2 and parent for that final solution)</li>\n<li>OpenCL (was looking promising, but implementing is sooo painful and takes too much time)</li>\n<li>first a month I had a bug: every public imported someone's notebook in the left top corner was always <code>false</code></li>\n<li>genetic (toooooo sloooooow).</li>\n</ul>\n<p>Let's discuss, which bugs did you do?<br>\nAnyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?</p>\n<h1>Thanks</h1>\n<p>Thanks for all public solution. Each of them was good start for my greedy solution to get small improvement 0.001 or even more..<br>\nEspecially, for: </p>\n<ul>\n<li><a href=\"https://www.kaggle.com/elvenmonk\" target=\"_blank\">@elvenmonk</a> <a href=\"https://www.kaggle.com/elvenmonk/pycosat-exploration\" target=\"_blank\">https://www.kaggle.com/elvenmonk/pycosat-exploration</a></li>\n<li><a href=\"https://www.kaggle.com/jamesmcguigan\" target=\"_blank\">@jamesmcguigan</a> <a href=\"https://www.kaggle.com/jamesmcguigan/game-of-life-z3-constraint-satisfaction\" target=\"_blank\">https://www.kaggle.com/jamesmcguigan/game-of-life-z3-constraint-satisfaction</a></li>\n<li><a href=\"https://www.kaggle.com/ulrich07\" target=\"_blank\">@ulrich07</a></li>\n<li><a href=\"https://www.kaggle.com/yakuben\" target=\"_blank\">@yakuben</a> </li>\n</ul>\n<p>SAT notebooks inspired me to Phase 4, otherwise I was keeping only gredy things.</p>\n<p>Thanks <a href=\"https://www.kaggle.com/golubev\" target=\"_blank\">@golubev</a>  (1st place) for inviting to participate here.</p>\n<p>Thanks team-mates for good perfect solutions, for additional CPUs and nice discussions.<br>\nEric's write-up: <a href=\"https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/200531\" target=\"_blank\">https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/200531</a> </p>\n<h1>References:</h1>\n<ul>\n<li>Notebook with final submission (binaries): <a href=\"https://www.kaggle.com/knstqq/knst-conways-rt\" target=\"_blank\">https://www.kaggle.com/knstqq/knst-conways-rt</a></li>\n<li>Commented version with source code: <a href=\"https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\" target=\"_blank\">https://www.kaggle.com/knstqq/conway-notebook-annealing-sat</a></li>\n<li>GitHub with more details and tools: (will be added soon)</li>\n<li>Solved delta=2: <a href=\"https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/190796\" target=\"_blank\">https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/190796</a></li>\n<li>Sat competition: <a href=\"https://satcompetition.github.io/2020/\" target=\"_blank\">https://satcompetition.github.io/2020/</a></li>\n<li>KISSAT: <a href=\"https://github.com/arminbiere/kissat/\" target=\"_blank\">https://github.com/arminbiere/kissat/</a></li>\n</ul>",
  "messages": [
    {
      "id": "1097117",
      "postDate": "12/01/2020 00:37:45",
      "content": "<h1>Intro</h1>\n<p>Welcome!<br>\nI'd like to present a solution, that gives a score 0.035 after just 9 hours of computation and goes to 0.015 after couple of weeks.<br>\nShortly, there was used a combination of SAT, greedy gradient optimization (annealing) and  hardcore optimizations.<br>\nFor developing was chosen C++ language as it seems a task that is critical for performance and every small advantage can be beneficial.</p>\n<p><em>for who'd like to read code rather than text:</em><br>\nNotebook with final submission (binaries): <a href=\"https://www.kaggle.com/knstqq/knst-conways-rt\" target=\"_blank\">https://www.kaggle.com/knstqq/knst-conways-rt</a><br>\nCommented version with source code: <a href=\"https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\" target=\"_blank\">https://www.kaggle.com/knstqq/conway-notebook-annealing-sat</a><br>\nGitHub with more details and tools: (will be added soon)</p>\n<h1>Phase 1. Simplicity</h1>\n<p>Let's take input and put it as output. ~0.17 is here. It is not too bad score, good baseline to start.<br>\nSome boards become periodic, so, choose delta+1, delta+2, … position and look to score. Tiny improvement: ~0.145<br>\nTrying more position: random fields, empty… Tiny improvement again: ~0.138</p>\n<h1>Phase 2. Greediness</h1>\n<p>Instead of generating completely new board, better to try to improve current one step-by-step. For the \"best\" position get by Phase 1 the each cell is changed one-by-one. If score is improved, changed is preserved, otherwise return previous state. To repeat it millions of times… ~0.105<br>\nLet's do it smarter. If score is not actually changed we preserve some changed values if it does not become worse. It will help to achieve position that need more than 1 bit to change without actually changing position to become worse. ~0.075 achieved.<br>\nThis solution is running on Ryzen 3900 and after long computations the score is improving, but very slowly… Good progress at the moment, but I need more!<br>\nTo avoid be stucked in local maximums I added some random noise after thousands of greedy iterations. Score became 0.068.</p>\n<h1>Phase 3. Stimulants</h1>\n<p>That works nice, but it is slower and slower with every hour, every day and at some moment it started to be that slow, that heating of my CPU lost attractiveness.<br>\nTime to put stimulants to solution.<br>\nIn implementation, the field is presented as linear array of N*N elements. And every time, when just a single position is changed, the score is heavily re-calculated.</p>\n<pre><code>const int dx8[] = {-1, -1, -1,  0,  0,  1,  1,  1};\nconst int dy8[] = {-1,  0,  1, -1,  1, -1,  0,  1};\nbool calc(const vector&lt;bool&gt;&amp; field, int pos) {\n    int x = pos % n;\n    int y = pos / n;\n    int count = 0;\n    for (int i = 0; i &lt; 8; ++i) {\n        int a = x + dx8[i];\n        int b = y + dy8[i];\n        while (a &lt; 0) a += n;\n        while (a &gt;= n) a -= n;\n        while (b &lt; 0) b += n;\n        while (b &gt;= n) b -= n;\n\n        if (field[b * n + a]) ++count;\n    }\n    if (count &lt; 2) return false;\n    if (count &gt; 3) return false;\n    if (field[pos]) return true;\n    if (count == 3) return true;\n    return false;\n}\nvector&lt;bool&gt; run_step(vector&lt;bool&gt; start) {\n    vector&lt;bool&gt; next(N, false);\n    for (int i = 0; i &lt; N; ++i) {\n        next[i] = calc(start, i);\n    }\n}\n</code></pre>\n<p>For sure, profiler shows that <code>run_step</code> and <code>calc</code> together are consuming 99% of CPU time.<br>\nAfter lots of runs benchmark, profiler (perf) and looking to generated asm code, this simple solution evolved and speeded up in 100x times:</p>\n<ul>\n<li>specified aggressive optimization options: <code>-O3 -march=native -funroll-loops -fno-omit-frame-pointer</code> (10% faster)</li>\n<li><code>vector&lt;bool&gt;</code> is too inefficient in C++, the replacement to <code>vector&lt;char&gt;</code> with 1 field in 1 byte works much faster. Even it consume more memory, but the asm code is much more efficient (20% faster)</li>\n<li>used <code>unsigned ints</code> let GCC to generate more efficient code rather than signed int due to more efficient asm code again (20% faster)</li>\n<li>used simple conditions <code>return (count | field[pos]) == 3</code> instead of 4 condition if-else (5% faster)</li>\n<li>specialized implementation for cells in the middle of board - because no need to check \"is it border\" heavily (+200% faster)</li>\n<li>for borders were implemented specialized optimized methods: one for left side, one for right side, for top side, for down side (+100% faster)</li>\n<li>implemented partial updates for only square with side <code>(2delta + 1)</code> cells (+1000% faster)</li>\n<li>instead of returning to previous position by re-calculation saving all changes to changelog and use it only if returning is needed (+5% faster)</li>\n</ul>\n<p>I stopped to write down achieved score here… So, whatever exact number is, but greediness becomes much faster. ~0.05 ?</p>\n<h1>Phase 4. Perfectness.</h1>\n<p>Until now perfect solution have been found very inefficient even for delta=1, just some good estimation.<br>\nTime to look for SAT.<br>\nThe winner of SAT Competition 2020 is KISSAT. There is no any other option for me rather then best sat solver at the moment.<br>\nIndeed, as checked someday later Z3 works well, but worse. Pycosat is much slower. Some other solver (I will not call their names) are much slower, some even just crashes with no reason.</p>\n<p>Each task is converted to CNF with hundreds of thousands of conjunctions. They are minimized sufficiently by sympy something like that:</p>\n<pre><code>$ python3\n&gt;&gt;&gt; from sympy import *\n&gt;&gt;&gt; x, y, z = symbols('x y z')\n&gt;&gt;&gt; simplify_logic((x &amp; y &amp; ~z) | (x &amp; ~y &amp; z), form=\"cnf\")\nx &amp; (y | z) &amp; (~y | ~z)\n</code></pre>\n<p>You just need to wait an hour until sympy will finish my rigorous conjunction (single-thread).</p>\n<p>Serialization throught files DIMACS is too slow (10 Mb CNF to deserialize/parse back), so, necessary objects are extract for kissat solver and staticly linked to my solution, called directly by C interfaces.<br>\nThat give me all solved delta=1 in less than 5 minutes (24 cores with eaten just 4Gb RAM).<br>\n99% of delta 2 is solved in 4 hours, and fully solved in 36 hours.<br>\nSome minor changes (added time-outs, pre-selecting tasks with low weight, with big empty space, etc) let to solve 50% of delta=3, 30% of delta=4, 20% of delta=5<br>\nScore is 0.03</p>\n<h1>Phase 5. Avidity</h1>\n<p>A beeing greedy is good in short-term, but some times you need to give away things… Don't be too greedy. Heheh.<br>\nLet score in greedy methods sometime drop down from maximum, to 1-2-3-4-5 lost poings… After some tuning and experiments I re-invent Annealing for optimization.<br>\nJump to 0.015 in less than 1 day of computation.</p>\n<h1>Phase 6. Revealing.</h1>\n<p>Until this moment I did not used sat more, but as was written in duscussion, lots of delta=3, 4, 5 can be solved.<br>\nAfter carefully chosed team members (and lots because their achievements) we jumped to 0.07: annealing for good aproximation is not good enough without \"perfect\" solutions.<br>\nScore: <strong>under 0.0064</strong> at the final (offline solution)</p>\n<h1>Phase 7. Rebirth.</h1>\n<p>In last day all code was re-written a lot of times, all params carefully adjusted. More than 100 tests on subset of data is done to find the best combination for real-time notebook solution<br>\nThanks <a href=\"https://www.kaggle.com/robertc\" target=\"_blank\">@robertc</a> for an advice to use \"checked\" position as start one helped significantly.<br>\nHere is it, *<em>the score 0.035</em> for notebook*.</p>\n<h1>Bonus: Dead Ends</h1>\n<p>Approaches that didn't worked well:</p>\n<ul>\n<li>partial SAT (specify not all fields, but only some to speed up SAT solver). It gives too bad approximation that worse than greedy solutions.</li>\n<li>step SAT (calculate for delta=3 as delta=2 and parent for that final solution)</li>\n<li>OpenCL (was looking promising, but implementing is sooo painful and takes too much time)</li>\n<li>first a month I had a bug: every public imported someone's notebook in the left top corner was always <code>false</code></li>\n<li>genetic (toooooo sloooooow).</li>\n</ul>\n<p>Let's discuss, which bugs did you do?<br>\nAnyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?</p>\n<h1>Thanks</h1>\n<p>Thanks for all public solution. Each of them was good start for my greedy solution to get small improvement 0.001 or even more..<br>\nEspecially, for: </p>\n<ul>\n<li><a href=\"https://www.kaggle.com/elvenmonk\" target=\"_blank\">@elvenmonk</a> <a href=\"https://www.kaggle.com/elvenmonk/pycosat-exploration\" target=\"_blank\">https://www.kaggle.com/elvenmonk/pycosat-exploration</a></li>\n<li><a href=\"https://www.kaggle.com/jamesmcguigan\" target=\"_blank\">@jamesmcguigan</a> <a href=\"https://www.kaggle.com/jamesmcguigan/game-of-life-z3-constraint-satisfaction\" target=\"_blank\">https://www.kaggle.com/jamesmcguigan/game-of-life-z3-constraint-satisfaction</a></li>\n<li><a href=\"https://www.kaggle.com/ulrich07\" target=\"_blank\">@ulrich07</a></li>\n<li><a href=\"https://www.kaggle.com/yakuben\" target=\"_blank\">@yakuben</a> </li>\n</ul>\n<p>SAT notebooks inspired me to Phase 4, otherwise I was keeping only gredy things.</p>\n<p>Thanks <a href=\"https://www.kaggle.com/golubev\" target=\"_blank\">@golubev</a>  (1st place) for inviting to participate here.</p>\n<p>Thanks team-mates for good perfect solutions, for additional CPUs and nice discussions.<br>\nEric's write-up: <a href=\"https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/200531\" target=\"_blank\">https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/200531</a> </p>\n<h1>References:</h1>\n<ul>\n<li>Notebook with final submission (binaries): <a href=\"https://www.kaggle.com/knstqq/knst-conways-rt\" target=\"_blank\">https://www.kaggle.com/knstqq/knst-conways-rt</a></li>\n<li>Commented version with source code: <a href=\"https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\" target=\"_blank\">https://www.kaggle.com/knstqq/conway-notebook-annealing-sat</a></li>\n<li>GitHub with more details and tools: (will be added soon)</li>\n<li>Solved delta=2: <a href=\"https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/190796\" target=\"_blank\">https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/190796</a></li>\n<li>Sat competition: <a href=\"https://satcompetition.github.io/2020/\" target=\"_blank\">https://satcompetition.github.io/2020/</a></li>\n<li>KISSAT: <a href=\"https://github.com/arminbiere/kissat/\" target=\"_blank\">https://github.com/arminbiere/kissat/</a></li>\n</ul>",
      "rawMarkdown": "# Intro\nWelcome!\nI'd like to present a solution, that gives a score 0.035 after just 9 hours of computation and goes to 0.015 after couple of weeks.\nShortly, there was used a combination of SAT, greedy gradient optimization (annealing) and  hardcore optimizations.\nFor developing was chosen C++ language as it seems a task that is critical for performance and every small advantage can be beneficial.\n\n*for who'd like to read code rather than text:*\nNotebook with final submission (binaries): https://www.kaggle.com/knstqq/knst-conways-rt\nCommented version with source code: https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\nGitHub with more details and tools: (will be added soon)\n\n# Phase 1. Simplicity\nLet's take input and put it as output. ~0.17 is here. It is not too bad score, good baseline to start.\nSome boards become periodic, so, choose delta+1, delta+2, ... position and look to score. Tiny improvement: ~0.145\nTrying more position: random fields, empty... Tiny improvement again: ~0.138\n\n# Phase 2. Greediness\nInstead of generating completely new board, better to try to improve current one step-by-step. For the \"best\" position get by Phase 1 the each cell is changed one-by-one. If score is improved, changed is preserved, otherwise return previous state. To repeat it millions of times... ~0.105\nLet's do it smarter. If score is not actually changed we preserve some changed values if it does not become worse. It will help to achieve position that need more than 1 bit to change without actually changing position to become worse. ~0.075 achieved.\nThis solution is running on Ryzen 3900 and after long computations the score is improving, but very slowly... Good progress at the moment, but I need more!\nTo avoid be stucked in local maximums I added some random noise after thousands of greedy iterations. Score became 0.068.\n\n# Phase 3. Stimulants\nThat works nice, but it is slower and slower with every hour, every day and at some moment it started to be that slow, that heating of my CPU lost attractiveness.\nTime to put stimulants to solution.\nIn implementation, the field is presented as linear array of N*N elements. And every time, when just a single position is changed, the score is heavily re-calculated.\n\n```\nconst int dx8[] = {-1, -1, -1,  0,  0,  1,  1,  1};\nconst int dy8[] = {-1,  0,  1, -1,  1, -1,  0,  1};\nbool calc(const vector<bool>& field, int pos) {\n    int x = pos % n;\n    int y = pos / n;\n    int count = 0;\n    for (int i = 0; i < 8; ++i) {\n        int a = x + dx8[i];\n        int b = y + dy8[i];\n        while (a < 0) a += n;\n        while (a >= n) a -= n;\n        while (b < 0) b += n;\n        while (b >= n) b -= n;\n\n        if (field[b * n + a]) ++count;\n    }\n    if (count < 2) return false;\n    if (count > 3) return false;\n    if (field[pos]) return true;\n    if (count == 3) return true;\n    return false;\n}\nvector<bool> run_step(vector<bool> start) {\n    vector<bool> next(N, false);\n    for (int i = 0; i < N; ++i) {\n        next[i] = calc(start, i);\n    }\n}\n```\nFor sure, profiler shows that `run_step` and `calc` together are consuming 99% of CPU time.\nAfter lots of runs benchmark, profiler (perf) and looking to generated asm code, this simple solution evolved and speeded up in 100x times:\n - specified aggressive optimization options: `-O3 -march=native -funroll-loops -fno-omit-frame-pointer` (10% faster)\n - `vector<bool>` is too inefficient in C++, the replacement to `vector<char>` with 1 field in 1 byte works much faster. Even it consume more memory, but the asm code is much more efficient (20% faster)\n - used `unsigned ints` let GCC to generate more efficient code rather than signed int due to more efficient asm code again (20% faster)\n - used simple conditions `return (count | field[pos]) == 3` instead of 4 condition if-else (5% faster)\n - specialized implementation for cells in the middle of board - because no need to check \"is it border\" heavily (+200% faster)\n - for borders were implemented specialized optimized methods: one for left side, one for right side, for top side, for down side (+100% faster)\n - implemented partial updates for only square with side `(2delta + 1)` cells (+1000% faster)\n - instead of returning to previous position by re-calculation saving all changes to changelog and use it only if returning is needed (+5% faster)\n\nI stopped to write down achieved score here... So, whatever exact number is, but greediness becomes much faster. ~0.05 ?\n\n# Phase 4. Perfectness.\nUntil now perfect solution have been found very inefficient even for delta=1, just some good estimation.\nTime to look for SAT.\nThe winner of SAT Competition 2020 is KISSAT. There is no any other option for me rather then best sat solver at the moment.\nIndeed, as checked someday later Z3 works well, but worse. Pycosat is much slower. Some other solver (I will not call their names) are much slower, some even just crashes with no reason.\n\nEach task is converted to CNF with hundreds of thousands of conjunctions. They are minimized sufficiently by sympy something like that:\n```\n$ python3\n>>> from sympy import *\n>>> x, y, z = symbols('x y z')\n>>> simplify_logic((x & y & ~z) | (x & ~y & z), form=\"cnf\")\nx & (y | z) & (~y | ~z)\n```\nYou just need to wait an hour until sympy will finish my rigorous conjunction (single-thread).\n\nSerialization throught files DIMACS is too slow (10 Mb CNF to deserialize/parse back), so, necessary objects are extract for kissat solver and staticly linked to my solution, called directly by C interfaces.\nThat give me all solved delta=1 in less than 5 minutes (24 cores with eaten just 4Gb RAM).\n99% of delta 2 is solved in 4 hours, and fully solved in 36 hours.\nSome minor changes (added time-outs, pre-selecting tasks with low weight, with big empty space, etc) let to solve 50% of delta=3, 30% of delta=4, 20% of delta=5\nScore is 0.03\n\n# Phase 5. Avidity\nA beeing greedy is good in short-term, but some times you need to give away things... Don't be too greedy. Heheh.\nLet score in greedy methods sometime drop down from maximum, to 1-2-3-4-5 lost poings... After some tuning and experiments I re-invent Annealing for optimization.\nJump to 0.015 in less than 1 day of computation.\n\n# Phase 6. Revealing.\nUntil this moment I did not used sat more, but as was written in duscussion, lots of delta=3, 4, 5 can be solved.\nAfter carefully chosed team members (and lots because their achievements) we jumped to 0.07: annealing for good aproximation is not good enough without \"perfect\" solutions.\nScore: **under 0.0064** at the final (offline solution)\n\n# Phase 7. Rebirth.\nIn last day all code was re-written a lot of times, all params carefully adjusted. More than 100 tests on subset of data is done to find the best combination for real-time notebook solution\nThanks @robertc for an advice to use \"checked\" position as start one helped significantly.\nHere is it, **the score 0.035* for notebook*.\n\n# Bonus: Dead Ends\nApproaches that didn't worked well:\n - partial SAT (specify not all fields, but only some to speed up SAT solver). It gives too bad approximation that worse than greedy solutions.\n - step SAT (calculate for delta=3 as delta=2 and parent for that final solution)\n - OpenCL (was looking promising, but implementing is sooo painful and takes too much time)\n - first a month I had a bug: every public imported someone's notebook in the left top corner was always `false`\n - genetic (toooooo sloooooow).\n\nLet's discuss, which bugs did you do?\nAnyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?\n\n\n# Thanks\nThanks for all public solution. Each of them was good start for my greedy solution to get small improvement 0.001 or even more..\nEspecially, for: \n - @elvenmonk https://www.kaggle.com/elvenmonk/pycosat-exploration\n - @jamesmcguigan https://www.kaggle.com/jamesmcguigan/game-of-life-z3-constraint-satisfaction\n - @ulrich07\n - @yakuben \n\nSAT notebooks inspired me to Phase 4, otherwise I was keeping only gredy things.\n\nThanks @golubev  (1st place) for inviting to participate here.\n\nThanks team-mates for good perfect solutions, for additional CPUs and nice discussions.\nEric's write-up: https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/200531 \n\n# References:\n - Notebook with final submission (binaries): https://www.kaggle.com/knstqq/knst-conways-rt\n - Commented version with source code: https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\n - GitHub with more details and tools: (will be added soon)\n - Solved delta=2: https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/190796\n - Sat competition: https://satcompetition.github.io/2020/\n - KISSAT: https://github.com/arminbiere/kissat/",
      "votes": null
    },
    {
      "id": "1097607",
      "postDate": "12/01/2020 06:02:31",
      "content": "<blockquote>\n  <p>Anyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?</p>\n</blockquote>\n<p>My final result is 100% \"step-by-step\" SAT and it can do better if more time was given.<br>\nI think I found set of additional optional constraints to make previous step solvable with high probability knowing that all start boards have another 5 previous steps, thus ideally solvable to depth \"delta+5\".<br>\nMy bad that I have not done good research on other available solvers to find and use KISSAT. I feel my solution could land somewhere in top 3 with faster SAT solver, especially the one that supports optional clauses.</p>",
      "rawMarkdown": "> Anyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?\n\nMy final result is 100% \"step-by-step\" SAT and it can do better if more time was given.\nI think I found set of additional optional constraints to make previous step solvable with high probability knowing that all start boards have another 5 previous steps, thus ideally solvable to depth \"delta+5\".\nMy bad that I have not done good research on other available solvers to find and use KISSAT. I feel my solution could land somewhere in top 3 with faster SAT solver, especially the one that supports optional clauses.",
      "votes": null
    }
  ],
  "comments": [
    {
      "id": 1097607,
      "author_name": "elvenmonk",
      "author_url": "",
      "post_date": "12/01/2020 06:02:31",
      "content": "<blockquote>\n  <p>Anyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?</p>\n</blockquote>\n<p>My final result is 100% \"step-by-step\" SAT and it can do better if more time was given.<br>\nI think I found set of additional optional constraints to make previous step solvable with high probability knowing that all start boards have another 5 previous steps, thus ideally solvable to depth \"delta+5\".<br>\nMy bad that I have not done good research on other available solvers to find and use KISSAT. I feel my solution could land somewhere in top 3 with faster SAT solver, especially the one that supports optional clauses.</p>",
      "votes": null,
      "replies": []
    }
  ],
  "raw_markdown_by_id": {
    "1097117": "# Intro\nWelcome!\nI'd like to present a solution, that gives a score 0.035 after just 9 hours of computation and goes to 0.015 after couple of weeks.\nShortly, there was used a combination of SAT, greedy gradient optimization (annealing) and  hardcore optimizations.\nFor developing was chosen C++ language as it seems a task that is critical for performance and every small advantage can be beneficial.\n\n*for who'd like to read code rather than text:*\nNotebook with final submission (binaries): https://www.kaggle.com/knstqq/knst-conways-rt\nCommented version with source code: https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\nGitHub with more details and tools: (will be added soon)\n\n# Phase 1. Simplicity\nLet's take input and put it as output. ~0.17 is here. It is not too bad score, good baseline to start.\nSome boards become periodic, so, choose delta+1, delta+2, ... position and look to score. Tiny improvement: ~0.145\nTrying more position: random fields, empty... Tiny improvement again: ~0.138\n\n# Phase 2. Greediness\nInstead of generating completely new board, better to try to improve current one step-by-step. For the \"best\" position get by Phase 1 the each cell is changed one-by-one. If score is improved, changed is preserved, otherwise return previous state. To repeat it millions of times... ~0.105\nLet's do it smarter. If score is not actually changed we preserve some changed values if it does not become worse. It will help to achieve position that need more than 1 bit to change without actually changing position to become worse. ~0.075 achieved.\nThis solution is running on Ryzen 3900 and after long computations the score is improving, but very slowly... Good progress at the moment, but I need more!\nTo avoid be stucked in local maximums I added some random noise after thousands of greedy iterations. Score became 0.068.\n\n# Phase 3. Stimulants\nThat works nice, but it is slower and slower with every hour, every day and at some moment it started to be that slow, that heating of my CPU lost attractiveness.\nTime to put stimulants to solution.\nIn implementation, the field is presented as linear array of N*N elements. And every time, when just a single position is changed, the score is heavily re-calculated.\n\n```\nconst int dx8[] = {-1, -1, -1,  0,  0,  1,  1,  1};\nconst int dy8[] = {-1,  0,  1, -1,  1, -1,  0,  1};\nbool calc(const vector<bool>& field, int pos) {\n    int x = pos % n;\n    int y = pos / n;\n    int count = 0;\n    for (int i = 0; i < 8; ++i) {\n        int a = x + dx8[i];\n        int b = y + dy8[i];\n        while (a < 0) a += n;\n        while (a >= n) a -= n;\n        while (b < 0) b += n;\n        while (b >= n) b -= n;\n\n        if (field[b * n + a]) ++count;\n    }\n    if (count < 2) return false;\n    if (count > 3) return false;\n    if (field[pos]) return true;\n    if (count == 3) return true;\n    return false;\n}\nvector<bool> run_step(vector<bool> start) {\n    vector<bool> next(N, false);\n    for (int i = 0; i < N; ++i) {\n        next[i] = calc(start, i);\n    }\n}\n```\nFor sure, profiler shows that `run_step` and `calc` together are consuming 99% of CPU time.\nAfter lots of runs benchmark, profiler (perf) and looking to generated asm code, this simple solution evolved and speeded up in 100x times:\n - specified aggressive optimization options: `-O3 -march=native -funroll-loops -fno-omit-frame-pointer` (10% faster)\n - `vector<bool>` is too inefficient in C++, the replacement to `vector<char>` with 1 field in 1 byte works much faster. Even it consume more memory, but the asm code is much more efficient (20% faster)\n - used `unsigned ints` let GCC to generate more efficient code rather than signed int due to more efficient asm code again (20% faster)\n - used simple conditions `return (count | field[pos]) == 3` instead of 4 condition if-else (5% faster)\n - specialized implementation for cells in the middle of board - because no need to check \"is it border\" heavily (+200% faster)\n - for borders were implemented specialized optimized methods: one for left side, one for right side, for top side, for down side (+100% faster)\n - implemented partial updates for only square with side `(2delta + 1)` cells (+1000% faster)\n - instead of returning to previous position by re-calculation saving all changes to changelog and use it only if returning is needed (+5% faster)\n\nI stopped to write down achieved score here... So, whatever exact number is, but greediness becomes much faster. ~0.05 ?\n\n# Phase 4. Perfectness.\nUntil now perfect solution have been found very inefficient even for delta=1, just some good estimation.\nTime to look for SAT.\nThe winner of SAT Competition 2020 is KISSAT. There is no any other option for me rather then best sat solver at the moment.\nIndeed, as checked someday later Z3 works well, but worse. Pycosat is much slower. Some other solver (I will not call their names) are much slower, some even just crashes with no reason.\n\nEach task is converted to CNF with hundreds of thousands of conjunctions. They are minimized sufficiently by sympy something like that:\n```\n$ python3\n>>> from sympy import *\n>>> x, y, z = symbols('x y z')\n>>> simplify_logic((x & y & ~z) | (x & ~y & z), form=\"cnf\")\nx & (y | z) & (~y | ~z)\n```\nYou just need to wait an hour until sympy will finish my rigorous conjunction (single-thread).\n\nSerialization throught files DIMACS is too slow (10 Mb CNF to deserialize/parse back), so, necessary objects are extract for kissat solver and staticly linked to my solution, called directly by C interfaces.\nThat give me all solved delta=1 in less than 5 minutes (24 cores with eaten just 4Gb RAM).\n99% of delta 2 is solved in 4 hours, and fully solved in 36 hours.\nSome minor changes (added time-outs, pre-selecting tasks with low weight, with big empty space, etc) let to solve 50% of delta=3, 30% of delta=4, 20% of delta=5\nScore is 0.03\n\n# Phase 5. Avidity\nA beeing greedy is good in short-term, but some times you need to give away things... Don't be too greedy. Heheh.\nLet score in greedy methods sometime drop down from maximum, to 1-2-3-4-5 lost poings... After some tuning and experiments I re-invent Annealing for optimization.\nJump to 0.015 in less than 1 day of computation.\n\n# Phase 6. Revealing.\nUntil this moment I did not used sat more, but as was written in duscussion, lots of delta=3, 4, 5 can be solved.\nAfter carefully chosed team members (and lots because their achievements) we jumped to 0.07: annealing for good aproximation is not good enough without \"perfect\" solutions.\nScore: **under 0.0064** at the final (offline solution)\n\n# Phase 7. Rebirth.\nIn last day all code was re-written a lot of times, all params carefully adjusted. More than 100 tests on subset of data is done to find the best combination for real-time notebook solution\nThanks @robertc for an advice to use \"checked\" position as start one helped significantly.\nHere is it, **the score 0.035* for notebook*.\n\n# Bonus: Dead Ends\nApproaches that didn't worked well:\n - partial SAT (specify not all fields, but only some to speed up SAT solver). It gives too bad approximation that worse than greedy solutions.\n - step SAT (calculate for delta=3 as delta=2 and parent for that final solution)\n - OpenCL (was looking promising, but implementing is sooo painful and takes too much time)\n - first a month I had a bug: every public imported someone's notebook in the left top corner was always `false`\n - genetic (toooooo sloooooow).\n\nLet's discuss, which bugs did you do?\nAnyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?\n\n\n# Thanks\nThanks for all public solution. Each of them was good start for my greedy solution to get small improvement 0.001 or even more..\nEspecially, for: \n - @elvenmonk https://www.kaggle.com/elvenmonk/pycosat-exploration\n - @jamesmcguigan https://www.kaggle.com/jamesmcguigan/game-of-life-z3-constraint-satisfaction\n - @ulrich07\n - @yakuben \n\nSAT notebooks inspired me to Phase 4, otherwise I was keeping only gredy things.\n\nThanks @golubev  (1st place) for inviting to participate here.\n\nThanks team-mates for good perfect solutions, for additional CPUs and nice discussions.\nEric's write-up: https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/200531 \n\n# References:\n - Notebook with final submission (binaries): https://www.kaggle.com/knstqq/knst-conways-rt\n - Commented version with source code: https://www.kaggle.com/knstqq/conway-notebook-annealing-sat\n - GitHub with more details and tools: (will be added soon)\n - Solved delta=2: https://www.kaggle.com/c/conways-reverse-game-of-life-2020/discussion/190796\n - Sat competition: https://satcompetition.github.io/2020/\n - KISSAT: https://github.com/arminbiere/kissat/",
    "1097607": "> Anyone make successfully \"step-by-step SAT\" or \"genetic\" with good precision?\n\nMy final result is 100% \"step-by-step\" SAT and it can do better if more time was given.\nI think I found set of additional optional constraints to make previous step solvable with high probability knowing that all start boards have another 5 previous steps, thus ideally solvable to depth \"delta+5\".\nMy bad that I have not done good research on other available solvers to find and use KISSAT. I feel my solution could land somewhere in top 3 with faster SAT solver, especially the one that supports optional clauses."
  },
  "source": "meta"
}