Fortranでじゃんけん型セル・オートマトン


これまでセル・オートマトンで遊ぶ記事をいくつか書いていたのですが、
カレンダーの穴埋めに、
この流れでもうひとつ。

「 [Chainerとcupyをcuda版numpyとして使ってオートマトン](https://qiita.com/sage-git/items/b4c01dc1c7ebec59c0d2)」のモデルを「[Fortranでライフゲーム](https://qiita.com/sage-git/items/5c668a78d75a1b0aaaf1)」を踏まえてFortranで実装します。



# セル・オートマトン

セル・オートマトンは、以下を定義したセルを並べて時間発展させることによって遊ぶことのできるモデルです。

* 個々の状態
* 他のセルとの相互作用で自分の状態がどう変わるか

プログラムではこのルールしか決めていないのに、集団になると多彩な現象をみせる不思議さが人気の秘密です。

詳しくは[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%BB%E3%83%AB%E3%83%BB%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3)などをご参照ください。
Wikipediaでは、状態が生死の２種で23/3ルールの俗に言う[ライフゲーム](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)（Conway's Game of Life）が妙に詳しいです。ライフゲーム中に出てくるパターンそれぞれに専用ページがあるレベル。

ここでは個人的にじゃんけんオートマトンと認識している、３すくみのオートマトンを実装します。
例えば https://qiita.com/SouTakenaka/items/a7dcfbde0a08063f4d41 にあるように、BZ反応のシミュレーションに用いられたりします。

ですが今回は
https://www.youtube.com/watch?v=zhxSyUL3OOY
にあるように、渦を作って遊ぶだけにしておきます。


# セルの種類と相互作用

正方格子にセルを並べ、隣接セルをマインスイーパ式（つまり上下左右と斜め４つの系）とします。

それぞれのセルについて以下の４種類を定義します。

![graph.png](https://qiita-image-store.s3.amazonaws.com/0/258019/8534dca2-7d9a-f33d-a2e9-b03d1c0fd66f.png)

また、セルはHPというパラメータを持っています。このパラメータは隣接セルとの相互作用により減らされます。

HPが０になると、セルの状態は上図の矢印の方向に状態が変異します。

なお、石以外の紙・ハサミと隣接したEmptyセルも石になります。複数種類のセルと隣接したEmptyのルールとか、あと「emptyセルがどのセルと隣接していたか」を管理する分プログラムが面倒になりますので、一律に石に変化するとしました。

ちなみにEMPTYは初期配置にしか現れないということにしました。

## HPについて

Emptyは他の石・紙・ハサミセルから、また石・紙・ハサミセルはじゃんけんで負けてしまう相手から１つのセルにつき１減らされます。
例えば紙セル４つと隣接した石セルは１ターンでHPを４減らされます。また、ハサミセルを包囲しているすべての紙セルは１ずつHPを減らされます。

変異した直後はHPを１０とします。

## 初期配置

０〜３の整数をランダムで作って並べます。

# 実装

アルゴリズムの各段階のスニペットを解説します。全部まとめてのプログラムもあります。

## 盤面準備

```fortran
        W = width
        H = height
        allocate(RPS(W, H))
        allocate(HP(W, H))
        RPS(:, :) = 0
        HP(:, :) = 1
```

`RPS`がセルの種類を保持します。
`HP`がセルのHPを保持します。

とりあえず全部のセルをHP1のEMPTYにしています。

## 初期配置

何の変哲もないループです。

```fortran
        integer :: x, y
        double precision :: p
        do x = 1, W
            do y = 1, H
                call random_number(p)
                RPS(x, y) = int(p*4.d0)
            enddo
        enddo
```

## 数え上げ

「[Fortranでライフゲーム](https://qiita.com/sage-git/items/5c668a78d75a1b0aaaf1)」では、周囲のセルをカウントするとき、

```fortran
        neighbors = 0
        do concurrent (dx = -1:1, dy = -1:1)
            neighbors = neighbors + cshift(cshift(field, dx, 1), dy, 2)
        enddo
```

としましたが、これは一時的にとはいえ`cshift`した結果の配列が毎回生成される気がしたので、愚直にループを回すことにしました。
あと、`cshift`を用いると周期境界条件が簡単に実現できますが、自作`do`ループ数え上げについては配列端での処理が面倒になります。そのため、今回は端で切れている孤立系です。

```fortran
        do concurrent (x=1:W, y=1:H)
            do concurrent (dx = max(1,x-1):min(W,x+1), dy = max(1,y-1):min(H,y+1))
                if( dx == x .and. dy == y) cycle
                i = RPS(x, y) + 1
                if(i == 4) i = 1
                if(RPS(dx, dy) == i) neighbor(x, y) = neighbor(x, y) + 1
                if(RPS(x, y) == 0 .and. RPS(dx, dy) > 0) neighbor(x, y) = neighbor(x, y) + 1
            enddo
        enddo
```

`do concurrent`は２次元の`do`ループを１つにまとめるためだけで、特に並列化とかは狙っていません。
一番外側のループは`x`, `y`について回すもので、システム全体をスキャンします。
その内側の`dx`、`dy`は注目している`x`、`y`について隣接するセルをスキャンします。上下左右に±1してループするので９回ループですが、`dx = dy = 0`となるのは自分自身を参照しているので`cycle`でとっとと次に送ります。配列の端に来たときに１ずらすと配列の外を参照してしまう可能性がありますので、`min`や`max`を使ってはみ出ないようにします。　

`i`は`(x, y)`のセルに勝てる種類を整数ラベルで持っています。`(dx, dy)`が`i`であれば`(x, y)`に1ダメージを与えます。
　つまり各セルごとに問題となる種類のセルを数え上げることになるので、これを`neighbor`にたくわえていきます。
　あと、EMPTYセルはEMPTYでないセルから無条件にダメージを喰らいますので、これも別に数えます。

変数のスコープについて。

* `x` `y` `dx` `dy` `i` `j`はループ毎に独立した変数
* `RPS`はモジュール変数で共通で変更なし
* `neighbor`はループ間で共用ですが集計のみの変更

`do concurrent`の使い方はこれでいいのだろうか。

## セルの変異

周りのセルの種類を数え上げたのちは、それに基づいてHP計算をします。

```fortran
       HP(:, :) = HP(:, :) - neighbor(:, :)
```

HPが０になったセルを変異させます。
単純に種類変数を１ふやし、４になったものを１に戻してやれば、３すくみが再現できます。

```fortran
        logical :: mute(W, H)
        integer, parameter :: MAX_HP = 10
        mute(:, :) = HP(:, :) < 1
        where(mute) RPS(:, :) = RPS(:, :) + 1
        where(RPS == 4) RPS = 1
        where(mute) HP = MAX_HP
```

## 画像生成

最後に可視化のための関数。

```fortran
    subroutine set_image(img)
        character(len=1), intent(inout) :: img(3, W, H)
        integer :: x, y, s
        do concurrent(x=1:W, y=1:H)
            s = RPS(x, y)
            
            if(s == 0) img(:, x, y) = EMPTY_COLOR
            if(s == 1) img(:, x, y) = ROCK_COLOR
            if(s == 2) img(:, x, y) = PAPER_COLOR
            if(s == 3) img(:, x, y) = SCISSOR_COLOR
        enddo
    end subroutine set_image
```

ここで、色は以下のようにセットしました。

```fortran
    character(len=1), parameter :: EMPTY_COLOR(3) = char([0, 0, 0])  !B,G,R
    character(len=1), parameter :: ROCK_COLOR(3) = char([12, 0, 179])  !B,G,R
    character(len=1), parameter :: PAPER_COLOR(3) = char([216, 216, 216])
    character(len=1), parameter :: SCISSOR_COLOR(3) = char([0, 179, 44])
```

クリスマスに合わせて、赤・緑・白のクリスマスカラーにしました。

* `EMPTY`は黒。
* `ROCK`は赤。サンタの服とかトナカイの鼻とか。
* `PAPER`は白。若干灰というか銀色。雪ですね。
* `SCISSOR`は緑。もみの木の葉のイメージ。

実際の色は「セルの種類と相互作用」節のグラフをご参照ください。ノードの背景色を対応する色にしています。

以上が`automaton`モジュールにまとめておく処理群になります。

## 初期化と時間発展

要はメイン関数に書くもの。

```fortran
    H = 256
    W = 256
    N = 800
    call init_system(H, W) ! 盤面準備
    call random_init()     ! 初期配置
    do i = 1, N
        call step()        ! 数え上げと変異
        ! ここで可視化
    enddo
```

このように、とりあえず256x256の盤面で800ステップ時間発展させてみます。

## ソースコード全体

以上のアルゴリズムを、Fortranのプログラムとして実行できるように整え、またOpenCVを使ってリアルタイムで表示できるようにします。

```fortran:automaton.f90
module rps_automaton
    implicit none
    integer, allocatable :: RPS(:, :), HP(:, :)
    integer :: W, H
    integer, parameter :: MAX_HP = 10
    character(len=1), parameter :: EMPTY_COLOR(3) = char([0, 0, 0])  !B,G,R
    character(len=1), parameter :: ROCK_COLOR(3) = char([12, 0, 179])  !B,G,R
    character(len=1), parameter :: PAPER_COLOR(3) = char([216, 216, 216])
    character(len=1), parameter :: SCISSOR_COLOR(3) = char([0, 179, 44])

contains
    subroutine init_system(height, width)
        integer, intent(in) :: height, width
        W = width
        H = height
        allocate(RPS(W, H))
        allocate(HP(W, H))
        RPS(:, :) = 0
        HP(:, :) = 1
    end subroutine init_system

    subroutine step()
        integer :: neighbor(W, H)
        integer :: x, y, dx, dy, i, j
        neighbor(:, :) = 0
        do concurrent (x=1:W, y=1:H)
            do concurrent (dx = max(1,x-1):min(W,x+1), dy = max(1,y-1):min(H,y+1))
                if( dx == x .and. dy == y) cycle
                i = RPS(x, y) + 1
                if(i == 4) i = 1
                if(RPS(dx, dy) == i) neighbor(x, y) = neighbor(x, y) + 1
                if(RPS(x, y) == 0 .and. RPS(dx, dy) > 0) neighbor(x, y) = neighbor(x, y) + 1
            enddo
        enddo
        HP(:, :) = HP(:, :) - neighbor(:, :)
        call mutation(HP < 1)
    end subroutine step

    subroutine mutation(mute)
        logical, intent(in) :: mute(W, H)
        where(mute) RPS(:, :) = RPS(:, :) + 1
        where(RPS == 4) RPS = 1
        where(mute) HP = MAX_HP
    end subroutine mutation

    subroutine random_init()
        integer :: x, y
        double precision :: p
        do x = 1, W
            do y = 1, H
                call random_number(p)
                RPS(x, y) = int(p*4.d0)
            enddo
        enddo
    end subroutine random_init

    subroutine set_image(img)
        character(len=1), intent(inout) :: img(3, W, H)
        integer :: x, y, s
        do concurrent(x=1:W, y=1:H)
            s = RPS(x, y)
            
            if(s == 0) img(:, x, y) = EMPTY_COLOR
            if(s == 1) img(:, x, y) = ROCK_COLOR
            if(s == 2) img(:, x, y) = PAPER_COLOR
            if(s == 3) img(:, x, y) = SCISSOR_COLOR
        enddo
    end subroutine set_image

end module rps_automaton
```

```fortran:main.f90
program main
    use rps_automaton, only: init_system, step, random_init, set_image
    use cv_types_c
    use cv_highgui_c
    use cv_core_c
    use iso_c_binding
    implicit none
    integer :: N, i
    integer(c_int) :: H, W, ret
    character(len=1), pointer :: img(:, :, :)
    type(c_ptr) :: p_img
    type(CvMat), pointer :: cvimg

    H = 256
    W = 256
    N = 800

    p_img = cvCreateMat(H, W, CV_MAKETYPE(CV_8U, 3))
    call c_f_pointer(p_img, cvimg)
    call c_f_pointer(cvimg%data, img, (/3, W, H/))

    call init_system(H, W)
    call random_init()
    do i = 1, N
        call step()
        call set_image(img)
        call cvShowImage("hoge", p_img)       
        ret = cvWaitKey(10)
    enddo
    call cvDestroyAllWindows()
    call cvReleaseMat(p_img)

end program main
```

あと、[FortranからOpenCVを叩く　画像処理関数編](https://qiita.com/sage-git/items/b97efb62bec4471f51cc)を参考[^1]に、`core_c.f90`と`highgui_c.f90`、`types_c.f90`を同じフォルダに準備します。

## コンパイルと実行

```
gfortran types_c.f90 core_c.f90 imgcodecs_c.f90 highgui_c.f90 automaton.f90 main.f90 -L/usr/local/lib -lopencv_core -lopencv_highgui
./a.out
```
Fortranのソースコードを一度にコンパイルするときは、引数でのファイルの順番に気をつけてください。例えば`types_c.f90`を`core_c.f90`の後に持ってくると、「`Fatal Error: Can't open module file ‘cv_types_c.mod’ for reading at (1): そのようなファイルやディレクトリはありません`」と言われます。
OpenCVのGUIウィンドウが立ち上がり、セル・オートマトンの様子がアニメーションで見れるはずです。


# スナップショット

以下の画像を用意するにあたり、デスクトップのスクリーンキャプチャではうまく行かなかったので、
[FortranからOpenCVを叩く　画像処理関数編](https://qiita.com/sage-git/items/b97efb62bec4471f51cc)の`cvImWrite`を使いました。

* 1フレーム目
![frame100001.png](https://qiita-image-store.s3.amazonaws.com/0/258019/b89d1ad2-8b12-4a82-b8fe-b8e94d0857df.png)

* 6フレーム目
![frame100006.png](https://qiita-image-store.s3.amazonaws.com/0/258019/36c42736-134f-e272-b3c7-2100f1a5ff47.png)

* 12フレーム目
![frame100012.png](https://qiita-image-store.s3.amazonaws.com/0/258019/530dd6bb-d710-f9d8-cf0c-6367a7563359.png)

* 20フレーム目
![frame100020.png](https://qiita-image-store.s3.amazonaws.com/0/258019/1dd3a08b-8b19-e707-6fb8-4490a07c6764.png)

* 30フレーム目
![frame100030.png](https://qiita-image-store.s3.amazonaws.com/0/258019/51de7b78-a47b-e7a8-08f3-b690cc26ad40.png)

* 45フレーム目
![frame100045.png](https://qiita-image-store.s3.amazonaws.com/0/258019/c82e4cc2-b613-8b84-e6d4-89af17a93f94.png)

* 70フレーム目
![frame100070.png](https://qiita-image-store.s3.amazonaws.com/0/258019/13f105ea-d676-68b5-7186-86bcf972c82d.png)

* 120フレーム目
![frame100120.png](https://qiita-image-store.s3.amazonaws.com/0/258019/5d9868ee-0ffe-48d8-4fd7-53cd761e4c88.png)

* 500フレーム目
![frame100500.png](https://qiita-image-store.s3.amazonaws.com/0/258019/a6148111-72eb-3938-2e29-321b7f70a60e.png)

* 800フレームまでのアニメーション
![test.gif](https://qiita-image-store.s3.amazonaws.com/0/258019/51a982c3-b0ed-9b6d-2edd-b239c504eeb4.gif)


ファイルサイズの関係で、フレームを間引きしています。

# 補足

このセルと相互作用の定義に行き着くまでに、六角格子ですが前にQiitaに投稿した「[人工生命を実装して世界大戦、人類滅亡への一歩を踏み出す](https://qiita.com/sage-git/items/183999c1006999959d68)」で試行錯誤しています。特にHPシステムとか。

ちなみにこの六角格子とかcupy版とかですが、セルの突然変異とか入れているので、決定論的ではないです。初期配置でセル３種類の配置で勝負し、突然変異なしというのが本来のセル・オートマトンかなと思います。まあ突然変異確率を減衰するようにしておりますので、これが十分小さくなるまではセル・オートマトンの初期配置生成段階、あるいは撹拌を再現していると思えばいいかなと。

# その他

cupy版のやつで大きいサイズで長時間動かしてたら、渦の中心がゆっくり移動しているように見えたのですが、これはどんなモデルで表現できるんだろうか。

# 終わりに

クリスマスらしい色合いにしましたので、クリスマスらしさを感じられていただけたら幸いです。

[^1]: highgui_c.f90の実装において、`cvWaitKey(msec)`の`msec`に`value`属性を付け忘れていました。すみません。