Rust でごった煮マシン



ふだん Ruby ばかり使っている私が [Rust](https://www.rust-lang.org/en-US/) で何か作ってみたくなった。

以下のような条件を満たす題材を考えた。

- Ruby でやるにはちょっと辛い計算量がある。
- 結果が目で見えて「なんか作った」気がする。
- Rust をちょっぴり勉強しただけの私にもどうにか作れそう。

「ごった煮マシン」を作ろうと思いついた。

# ごった煮マシンとは

ごった煮マシン（の概念およびそういう訳語）は次の文献で紹介されたもの。

『コンピューターレクリエーションII　遊びの探索』別冊サイエンス92，A.K. デュードニー 著，日経サイエンス（1989）

いわゆるセルラーオートマトンの一種。

縦横に並んだセルのそれぞれが整数値を持ち，時刻とともに変化していく。

あるセルの次の時刻の値は周囲のセルの値との兼ね合いで決まる。

具体的なアルゴリズムは省略。パラメーターが三つあって，それを変えると推移の様子が劇的に変わったりする。

ベロウソフ・ジャボチンスキー反応（BZ 反応）という，二次元的にきれいな模様が形成される化学反応のシミュレーションとして考案されたらしい。

※結果がすぐ見たい方は「[できた](#%E3%81%A7%E3%81%8D%E3%81%9F)」の節に飛んでください。

# 開発方針

セルラーオートマトンの計算はかなり単純なものなので，公式教材の日本語版「[プログラミング言語Rust](https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/README.html)」をある程度勉強したくらいでなんとかなりそう。

結果は，時刻ごとのセルの格子を PNG 画像として書き出すことにする。
画像ファイルの生成はちょっと難しそうだけど，実は [image](https://crates.io/crates/image) というライブラリーに，たった 50 行でジュリア集合の画像を書き出す [julia.rs](https://github.com/PistonDevelopers/image/blob/master/examples/fractal.rs) というサンプルがあったので，その出力部分を丸写しすることにした。
というか，このサンプルを見たからごった煮マシンをやろうと考えついたんだけどね。

残念ながら，この image ってやつは gcc の無い環境ではインストールできない。だから，Windows で動かすのは私には難しくて諦めた。Mac でやることにした。

各時刻の PNG 画像が生成できたら，それを [APNG Assembler](http://apngasm.sourceforge.net/) を使って APNG（Animation PNG）[^apng]にする。動画だぜ。うまくいけば「何か作った気がする」だろ？

[^apng]: APNG は Google Chrome や Edge ではアニメーションとして表示できない。Firefox や最新の Safari で見てほしい。


# 作る

まずはこれ。

```bash
cargo new gotta --bin
```

依存ライブラリーとして，rand と image。

```toml:Cargo.toml
[dependencies]
rand = "0.3.0"
image = "0.10.0"
```

ソースコードはこれだけ。

```rust:src/main.rs
extern crate rand;
extern crate image;

use rand::Rng;

use std::fs::File;
use std::path::Path;


struct Board {
    width: u32,
    height: u32,
    buff: Vec<u8>,
}

struct BoardParameter {
    k1: f32,
    k2: f32,
    g: u8,
}

impl Board {
    fn new(width: u32, height: u32) -> Board {
        Board {
            width: width,
            height: height,
            buff: vec![0; (width * height) as usize]
        }
    }

    fn seed(&mut self) {
        let mut rng = rand::thread_rng();
        for i in 0..self.buff.iter().count() {
            self.buff[i] = rng.gen();
        }
    }

    fn value(&self, x: u32, y: u32) -> u8 {
        self.buff[(y * self.width + x) as usize]
    }

    fn set_value(&mut self, x: u32, y: u32, value: u8) {
        self.buff[(y * self.width + x) as usize] = value;
    }

    fn copy_buff(&mut self, other: &Board) {
      for (i, v) in other.buff.iter().enumerate() {
        self.buff[i] = *v;
      }
    }

    fn image(&self, filename: &str) {
        let mut imgbuf = image::ImageBuffer::new(self.width, self.height);
        for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
             *pixel = image::Luma([self.value(x, y)]);
        }
        let ref mut fout = File::create(&Path::new(filename)).unwrap();
        let _ = image::ImageLuma8(imgbuf).save(fout, image::PNG);
    }

    fn neighborhood(&self, x: u32, y: u32) -> [u8; 8] {
        let x1: u32 = (x + self.width - 1) % self.width;
        let x2: u32 = (x + 1) % self.width;
        let y1: u32 = (y + self.height - 1) % self.height;
        let y2: u32 = (y + 1) % self.height;
        [
          self.value(x1, y1), self.value(x, y1), self.value(x2, y1),
          self.value(x1, y),                     self.value(x2, y),
          self.value(x1, y2), self.value(x, y2), self.value(x2, y2)
        ]
    }

    fn count_infected(&self, x: u32, y: u32) -> u8 {
        let mut count: u8 = 0;
        for value in &self.neighborhood(x, y) {
            if (*value > 0) & (*value < 255) { count += 1; }
        }
        count
    }

    fn count_illed(&self, x: u32, y: u32) -> u8 {
        let mut count: u8 = 0;
        for value in &self.neighborhood(x, y) {
            if *value == 255 { count += 1; }
        }
        count
    }

    fn sum(&self, x: u32, y: u32) -> u16 {
        self.neighborhood(x, y).iter().fold(0u16, |sum, v| sum + *v as u16) + self.value(x, y) as u16
    }

    fn step(&mut self, params: &BoardParameter) {
        let mut next_board = Board::new(self.width, self.height);
        let mut value: u8;
        for y in 0..self.height {
            for x in 0..self.width {
                value = self.value(x, y);
                if value == 255 {
                    next_board.set_value(x, y, 0);
                } else {
                    let c_infected = self.count_infected(x, y) as f32;
                    let c_illed = self.count_illed(x, y) as f32;
                    let mut next_value: u16;
                    if value == 0 {
                        let n1 = (c_infected / params.k1).floor() as u16;
                        let n2 = (c_illed / params.k2).floor() as u16;
                        next_value = n1 + n2;
                    } else {
                        let sum = self.sum(x, y) as f32;
                        next_value = (sum / c_infected).floor() as u16 + params.g as u16;
                    }
                    if next_value > 255 {
                        next_value = 255;
                    }
                    next_board.set_value(x, y, next_value as u8)
                }
            }
        }
        self.copy_buff(&next_board);
    }
}


fn main() {
    let mut b = Board::new(200, 160);
    let params = BoardParameter{k1: 2.0, k2: 3.0, g: 3};
    b.seed();
    for i in 0..480 {
        let s = format!("png/foo-{:04}.png", i);
        b.image(&s);
        b.step(&params);
    }
}
```

画像サイズは `main` 関数の `Board::new` に与えている。

ごった煮マシンのパラメーターは同じく `BoardParameter` として与えている。

画像生成数（コマ数）は `for` に与えてある。

まあ，パラメーターを変えるたびにコンパイルし直すのがイケてないけど，最初なのでこれでいいことにする。

どんなパラメーターだと面白い結果になるのかは，よく調べてない。試してみたい人は，とりあえず k1, k2 の値はあまり変えないで，g の値をいろいろ変えてみて。

# 実行

このコードだと，PNG を格納する `png` ディレクトリーをあらかじめ作っておく必要があるので，

```bash
$ mkdir png
```

しておく。


最初，

```bash
$ cargo run
```

とやったら，がっかりするほど遅かった。これだとビルド時に最適化が入らないらしい。

```bash
$ cargo run --release
```

にしたら劇的に改善した。

次に APNG assembler で

```bash
$ apngasm gotta.png png/*.png -l1 1 12
```

とやって，APNG ファイル「gotta.png」を作る。

オプション `-l1` はループしない（最後のコマで止まる）こと，`1 12` は 1 フレームが 1/12 秒であるという指定らしい。

# できた

できた APNG アニメーションがこれ。

![gotta.png](https://qiita-image-store.s3.amazonaws.com/0/48101/45b8fae2-4d3c-6b09-19f2-dd3c2d9e111c.png)

ええと，Qiita 上だとなぜかリロードしてもアニメーションを繰り返してくれないみたい。

もう一度見たい方は画像をローカルに保存してから Firefox などで開いてください。

# 感想

コンパイラー言語は敷居が高いと思っていたし，Rust は借用といった概念が難しいと思っていたけど，思ったほど悩まずにできた。

image がよくできてたのと，Rust のコンパイラーのエーラメッセージが有用だったからかな。

これならまた何か作ってみようという気になる。