{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 畳み込み演算によるlifegameの生死判定\n", "\n", "\n", "[Ruby Numo (NUmerical MOdule)](https://github.com/ruby-numo) の[gnuplot-demo/misc/lifegame ](https://github.com/ruby-numo/gnuplot-demo/tree/master/misc/lifegame)のLifeGameクラスを見たとき、とてもシンプルで不思議なコードに引き込まれました。\n", "\n", "「これだけのコードでどうやって生死の判定をしているのだろう?」と、\n", "\n", "とても興味を持ったところで少し調べてみると「畳み込み演算」らしいことが分かりました。盤面の生死の状態を画素として捉えて、畳み込みフィルタによる画像処理を行うイメージです。好奇心の冷めないうちに、観察の記録をこの iRuby Notebook に書き留めておきます。\n", "\n", "\n", "次のコードは [class LifeGame](https://github.com/ruby-numo/gnuplot-demo/blob/master/misc/lifegame/lifegame.rb#L4) からクラスの部分を書き出したものです。盤面の初期化(initialize メソッド)と更新(update メソッド)のみのシンプルな構成です。\n", "\n", "```ruby\n", "require 'numo/narray'\n", "\n", "class LifeGame\n", "\n", " def initialize(nx,ny,m)\n", " @data = Numo::UInt8.zeros(ny,nx)\n", " @data[m..ny-1-m,m..nx-1-m] = Numo::UInt8.new(ny-2*m,nx-2*m).rand(2)\n", " @step = 0\n", " end\n", "\n", " def update\n", " b = Numo::UInt8.zeros(*@data.shape)\n", " b[1..-2,1..-2] =\n", " @data[0..-3,0..-3] + @data[0..-3,1..-2] + @data[0..-3,2..-1] +\n", " @data[1..-2,0..-3] + @data[1..-2,2..-1] +\n", " @data[2..-1,0..-3] + @data[2..-1,1..-2] + @data[2..-1,2..-1]\n", " @data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(@data)))\n", " @step += 1\n", " end\n", "\n", " attr_reader :data,:step\n", "end\n", "```\n", "\n", "\n", "## 盤面の初期化\n", "\n", "盤面の初期化はinitializeメソッドで行います。以降は小さな盤面を作成し、確認用のコードを直書きしながら確認していきます。" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccccc}\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[6,6]\n", "[[0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "require 'numo/narray'\n", "require 'numo/gnuplot'\n", "require \"base64\"\n", "\n", "data = Numo::UInt8.zeros(6,6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 盤面の初期値を設定\n", "\n", "0は死、1は生の状態を表します。実際のクラスでは盤面の初期状態を乱数によって生成していますが、ここでは1世代目の初期値として「ビーコン」と呼ばれる周期が2の振動子を設定します。 " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0], [0, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data[0..5,0..5] = [[0,0,0,0,0,0], \n", " [0,1,1,0,0,0], \n", " [0,1,1,0,0,0], \n", " [0,0,0,1,1,0],\n", " [0,0,0,1,1,0], \n", " [0,0,0,0,0,0]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 盤面をupdate\n", "\n", "updateメソッドでは、畳み込み演算用データ b を、 data と同じサイズで初期化。" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccccc}\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", " 0&0&0&0&0&0\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[6,6]\n", "[[0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b = Numo::UInt8.zeros(*data.shape)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 畳み込み演算\n", "\n", "畳み込み演算によって 8 方向のデータを畳み込み、セルの生死判定用の数値を作成しています。ここでは配列のビューを指定して操作が行われています。" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccc}\n", " 3&3&2&0\\\\\n", " 3&4&4&2\\\\\n", " 2&4&4&3\\\\\n", " 0&2&3&3\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[4,4]\n", "[[3, 3, 2, 0], \n", " [3, 4, 4, 2], \n", " [2, 4, 4, 3], \n", " [0, 2, 3, 3]]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b[1..-2,1..-2] =\n", " data[0..-3,0..-3] + data[0..-3,1..-2] + data[0..-3,2..-1] +\n", " data[1..-2,0..-3] + data[1..-2,2..-1] +\n", " data[2..-1,0..-3] + data[2..-1,1..-2] + data[2..-1,2..-1]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "生死判定用の数値を格納する場所は、実際の盤面サイズより、ひと回り小さな範囲に格納されます。" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccccc}\n", " 0&0&0&0&0&0\\\\\n", " 0&3&3&2&0&0\\\\\n", " 0&3&4&4&2&0\\\\\n", " 0&2&4&4&3&0\\\\\n", " 0&0&2&3&3&0\\\\\n", " 0&0&0&0&0&0\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[6,6]\n", "[[0, 0, 0, 0, 0, 0], \n", " [0, 3, 3, 2, 0, 0], \n", " [0, 3, 4, 4, 2, 0], \n", " [0, 2, 4, 4, 3, 0], \n", " [0, 0, 2, 3, 3, 0], \n", " [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2世代目の生死判定\n", "\n", "ここで畳み込み演算の結果から2世代目のセルの生死判定を行い、生存データのみを盤面に書き込んでいます。\n", "\n", "この1行だけで盤面全体の生死判定ができるとは、なんとシンプルで素敵なことでしょう。" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccccc}\n", " 0&0&0&0&0&0\\\\\n", " 0&1&1&0&0&0\\\\\n", " 0&1&0&0&0&0\\\\\n", " 0&0&0&0&1&0\\\\\n", " 0&0&0&1&1&0\\\\\n", " 0&0&0&0&0&0\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[6,6]\n", "[[0, 0, 0, 0, 0, 0], \n", " [0, 1, 1, 0, 0, 0], \n", " [0, 1, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 1, 0], \n", " [0, 0, 0, 1, 1, 0], \n", " [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(data)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3世代目の生死判定\n", "\n", "再びupdateメソッドによって3世代目のセルの生死判定を行います。「ビーコン」は2周期の振動子なので、ここで元の状態に戻ります。" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccccc}\n", " 0&0&0&0&0&0\\\\\n", " 0&1&1&0&0&0\\\\\n", " 0&1&1&0&0&0\\\\\n", " 0&0&0&1&1&0\\\\\n", " 0&0&0&1&1&0\\\\\n", " 0&0&0&0&0&0\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[6,6]\n", "[[0, 0, 0, 0, 0, 0], \n", " [0, 1, 1, 0, 0, 0], \n", " [0, 1, 1, 0, 0, 0], \n", " [0, 0, 0, 1, 1, 0], \n", " [0, 0, 0, 1, 1, 0], \n", " [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b = Numo::UInt8.zeros(*data.shape)\n", "b[1..-2,1..-2] =\n", " data[0..-3,0..-3] + data[0..-3,1..-2] + data[0..-3,2..-1] +\n", " data[1..-2,0..-3] + data[1..-2,2..-1] +\n", " data[2..-1,0..-3] + data[2..-1,1..-2] + data[2..-1,2..-1]\n", "data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(data)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 次世代の生死判定\n", "\n", "以後のupdateによって、2周期の振動子はこの状態を繰り返します。" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$$\\left(\\begin{array}{cccccc}\n", " 0&0&0&0&0&0\\\\\n", " 0&1&1&0&0&0\\\\\n", " 0&1&0&0&0&0\\\\\n", " 0&0&0&0&1&0\\\\\n", " 0&0&0&1&1&0\\\\\n", " 0&0&0&0&0&0\\\\\n", "\\end{array}\\right)$$" ], "text/plain": [ "Numo::UInt8#shape=[6,6]\n", "[[0, 0, 0, 0, 0, 0], \n", " [0, 1, 1, 0, 0, 0], \n", " [0, 1, 0, 0, 0, 0], \n", " [0, 0, 0, 0, 1, 0], \n", " [0, 0, 0, 1, 1, 0], \n", " [0, 0, 0, 0, 0, 0]]" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b = Numo::UInt8.zeros(*data.shape)\n", "b[1..-2,1..-2] =\n", " data[0..-3,0..-3] + data[0..-3,1..-2] + data[0..-3,2..-1] +\n", " data[1..-2,0..-3] + data[1..-2,2..-1] +\n", " data[2..-1,0..-3] + data[2..-1,1..-2] + data[2..-1,2..-1]\n", "data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(data)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# セルの生死判定について\n", "\n", "\n", "## 次世代の存在条件\n", "\n", "次の世代が存在する条件はつぎの2つです。\n", "\n", "* 死んでいるセルに、隣接する生きたセルが3つ存在すれば次の世代が誕生する。\n", "```ruby\n", "(b.eq 3)\n", "```\n", "* 生きているセルに、隣接する生きたセルが2つか3つ存在すれば、次の世代でも生存可能。\n", "```ruby\n", "(Numo::Bit.cast(data) & ((b.eq 2) | (b.eq 3)))\n", "```\n", "それ以外は次世代では生存できない。(死の状態)\n", "\n", "\n", "これらの条件から、storeする条件式はつぎのようになるはずです。\n", "```ruby\n", "(b.eq 3) | (Numo::Bit.cast(data) & ((b.eq 2) | (b.eq 3)))\n", "```\n", "\n", "ところが実際のコードは少し違います。\n", "```ruby\n", "@data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(@data)))\n", "```\n", "\n", "## カルノ図で確認\n", "\n", "確認のためカルノ図を描いてみます。\n", "\n", "図を描きやすいように、予め各条件を`a, b, c`に置き換えます。\n", "```ruby\n", "A = Numo::Bit.cast(data)\n", "B = b.eq 2\n", "C = b.eq 3\n", "\n", "C | (A & (B | C))\n", "```\n", "\n", "真理値表は次のようになります。\n", "\n", "|$A$|$B$|$C$|$OUT$|\n", "|:-:|:-:|:-:|:-:|\n", "|0|0|0| 0 |\n", "|0|0|1| 1 |\n", "|0|1|0| 0 |\n", "|0|1|1| 1 |\n", "|1|0|0| 0 |\n", "|1|0|1| 1 |\n", "|1|1|0| 1 |\n", "|1|1|1| 1 |\n", "\n", "真理値表を元にカルノ図を描くと\n", "\n", "|$A \\backslash BC$|$\\overline{B}$|$\\overline{B}$|$B$|$B$|\n", "|------|--|--|--|--|\n", "|0 | 0| 1| 1| 0|\n", "|1 | 0| 1| 1| 1|\n", "| |$\\overline{C}$|$C$|$C$|$\\overline{C}$|\n", "\n", "## 論理圧縮\n", "\n", "上記のカルノ図を使って論理圧縮を行うと次のようになります。\n", "\n", "```\n", "圧縮前の C | (A & (B | C)) は\n", "\n", "圧縮後に C | (A & B) となる。\n", "\n", "ここで置き換えを元に戻すと (b.eq 3) | (Numo::Bit.cast(data) & (b.eq 2)) となり\n", "```\n", "この条件は、実際のコードの\n", "```ruby\n", "@data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(@data)))\n", "```\n", "と同じ結果になることがわかります。\n", "\n", "\n", "\n", "# 実際の「ビーコン」の動作を確認" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "\"\"" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "require 'numo/narray'\n", "require 'numo/gnuplot'\n", "require \"base64\"\n", "\n", "class LifeGameTest\n", "\n", " def initialize(nx,ny,m)\n", " @data = Numo::UInt8.zeros(ny,nx)\n", " @data[0..5,0..5] = [[0,0,0,0,0,0], \n", " [0,1,1,0,0,0], \n", " [0,1,1,0,0,0], \n", " [0,0,0,1,1,0],\n", " [0,0,0,1,1,0], \n", " [0,0,0,0,0,0]]\n", " #@data[m..ny-1-m,m..nx-1-m] = Numo::UInt8.new(ny-2*m,nx-2*m).rand(2)\n", " @step = 0\n", " end\n", "\n", " def update\n", " b = Numo::UInt8.zeros(*@data.shape)\n", " b[1..-2,1..-2] =\n", " @data[0..-3,0..-3] + @data[0..-3,1..-2] + @data[0..-3,2..-1] +\n", " @data[1..-2,0..-3] + @data[1..-2,2..-1] +\n", " @data[2..-1,0..-3] + @data[2..-1,1..-2] + @data[2..-1,2..-1]\n", " @data.store((b.eq 3) | ((b.eq 2) & Numo::Bit.cast(@data)))\n", " @step += 1\n", " end\n", "\n", " attr_reader :data,:step\n", "end\n", "\n", "nx,ny = 6, 6 # don't modify\n", "life = LifeGameTest.new(nx,ny,1)\n", "filename = \"lifegameTest.gif\" \n", "\n", "Numo.gnuplot do\n", " reset\n", " set output: filename\n", " set term: \"gif\", animate:true, delay:50, size:[500,500]\n", " set :nokey\n", " set size: {ratio:1.0*ny/nx}\n", " set xrange: -1..nx\n", " set yrange: -1..ny\n", " unset :colorbox\n", " set palette_defined:'(0 \"white\", 1 \"green\")'\n", "\n", " 10.times do |i|\n", " life.update if i > 0\n", " set title:\"lifegame step=#{i}\"\n", " plot life.data, flipy:true, with:\"image\"\n", " end\n", " set :output\n", "end\n", "\n", "img = Base64.strict_encode64(File.binread(filename))\n", "IRuby.html %Q()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# まとめ\n", "\n", "numo-gnuplotで出会ったlifegameから、新しい知見を得ることができました。\n", "\n", "感謝!\n", "\n", "### 参考\n", "\n", "* [ruby-numo/gnuplot: Gnuplot wrapper for Ruby/Numo](https://github.com/ruby-numo/gnuplot)\n", "* [ruby-numo/gnuplot-demo: Ruby/Numo::Gnuplot Demo](https://github.com/ruby-numo/gnuplot-demo)\n", "* [ruby-numo/narray: Ruby/Numo::NArray - New NArray class library](https://github.com/ruby-numo/narray)\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Ruby 2.6.5", "language": "ruby", "name": "ruby" }, "language_info": { "file_extension": ".rb", "mimetype": "application/x-ruby", "name": "ruby", "version": "2.6.5" } }, "nbformat": 4, "nbformat_minor": 2 }