【AWS EC2】シミュレーションプログラムを動かしてみた

AWS

はじめに

最近AWSの学習を始めました。
前回はAWS Skill Builderのラボを使用しての学習でした。

終わってからは実際にコンソールに登録してみました。
無料で100クレジット貰えて、チュートリアルを完了すると追加で100クレジット貰えます。

追加のクレジットを貰うにあたって、EC2のチュートリアルがありました。

EC2はクラウドコンピューティングサービスですね。
主な利用方法としてはWebサーバーだと思いますが、せっかくなら常時稼働の利点も活かしていきたいと思いました。

そこで思い出したのが地球シミュレータです。
常にシミュレーションが連続して行われ続けているのは、コンピューターサイエンスにおけるロマンを感じますよね。

というわけで、クラウドサーバー上で常時何らかのシミュレーションを行い、リクエストがきた時は現状のシミュレーション状況を返すというWebサーバーを作ってみようと思います。

シミュレーション内容

以前見たシミュレーション動画にこのような物がありました。

このシミュレーションではPredator-Prey-Plantの3種の生命が定義されています。
それぞれ狐、兎、植物に例えたら身近かもしれません。

またこれらはPredatorがPreyを食べ、PreyがPlantを食べる三竦みの関係にあります。
各生命は繁殖し、繁殖するためにはエネルギーが必要で、エネルギーのために捕食するわけですね。

循環的捕食モデルとでもいうのでしょうか。

これらの関係が上手く構築されているとロトカ・ヴォルテラ方程式のような関係が見えてくるはずです。

ロトカ・ヴォルテラの方程式 – Wikipedia

この例では2種のみで、兎と植物の関係に例えられるでしょう。

植物が増えると食料が豊富になり兎の繁殖が頻繁に行われる。
兎が繁殖していくと食料の消費スピードが上がり植物が少なくなっていき、兎が食糧難になり飢えて個体数が減っていく。
兎が減ると消費スピードが緩やかになるので植物の繁殖スピードが上回り、植物が増える。

という関係が続いていくというものですね。

ではこの関係構築を目標にシミュレーション環境を作っていきましょう。

技術仕様

では今回構築していくシステムの内容を考えていきます。
簡単な仕様をAIと壁打ちして詰めていき、最終的な仕様書を作成してもらいました。

まず言語ですが、Python、Go、C#などいくつかの選択肢がありました。
Python : ライブラリが豊富
Go : 並列処理が得意、Webサーバー構築が容易
C# : 筆者がある程度の内容を把握できる

今回は勉強も兼ねてC#で開発していくことにしました。
Pythonのライブラリを使ってもよかったのですが、今回の簡易な実装ならすぐに構築出るだろうという見込みです。

C#の.NET環境ですが、こちらはLinuxなどの他のOSでも実行できるようになっており、EC2でも環境を整えれば実行できるようです。
またsystemdという仕組みを利用することでSSH接続を切った後も実行し続けるようです。

シミュレーション状態保存

シミュレーションの状態保存ですが、S3を使えばバージョニングできるためシミュレーションに最適化と思われました。
しかしS3に高頻度に書き込み続けるとコストが膨大になってしまうようなので、今回は断念しました。

なのでAPIで現在のシミュレーション状況を取得するのみとします。

API

今回はC#を用いて設計し、サーバー上の状態をAPIで取得することにしました。
なので今回使うフレームワークは.NET 9.0で、APIサーバーはASP.NET Core Web APIにしました。

APIの内容は、クライアントからのリクエストに応じてその瞬間の全エージェントのリスト(種類、ID、位置座標など)をJSON形式で返すようにします。

  • エンドポイント: GET http://<EC2のパブリックIP>/api/world
  • リクエスト形式: なし
  • レスポンス形式: JSON
  • レスポンスボディ (例):
    json { "worldTime": 12345, "agents": [ { "id": 1, "type": "Fox", "position": { "x": 10.5, "y": 0, "z": 25.1 } }, { "id": 2, "type": "Rabbit", "position": { "x": 15.2, "y": 0, "z": 20.8 } }, { "id": 3, "type": "Rabbit", "position": { "x": 16.0, "y": 0, "z": 21.3 } }, { "id": 4, "type": "Grass", "position": { "x": 15.5, "y": 0, "z": 19.9 } } ] }

コーディング

今回も仕様書からGemini CLIに作成してもらいました。

やったこと

  • 座標は小数だとJSONのデータ量がとても増えてしまうので、整数値フィールドに限定。
  • フィールド外まで植物が繁殖してメモリ超過になっていた。(移動制限はしていたが繁殖制限をしていなかった…)
  • 他のエージェントが居る場合そのマスには移動できないようになっていた。それだとスタックしてしまうため衝突判定は同一エージェントだけに限定した。
  • 狐や兎はいざという時(逃走や狩り)に移動速度を上げるように。その分エネルギー(空腹度)も消費するように。
  • 狐は兎を狩るだけではなく死体もエサとするように変更。なので兎の死後はエサとして動かないようにする。また腐敗して消えるように、数ターンは残す。シミュレーションでは別の色で表示する。
  • 狐の死体の探索範囲を広げる。臭いに敏感な要素を追加。
  • 餓死だけでなく寿命を追加。それぞれのエージェントは一定ターン後に死亡するように。

以下は実装してみたが計算量の問題で採用を見送った機能です。

  • 狐も死後は残るようにして、草の肥料として活用する?
  • 狐は兎が見つからない場合は草に隠れて兎を待つ→副次効果でその地域の草が絶滅しない。

パフォーマンス向上

  • 周辺探索をした時に候補数が多くなってしまう(探索径×探索径×エージェント数)。なのでいくつかの候補に抽選して絞る。
  • マルチスレッド化。
  • 周辺探索処理の高速化

周辺探索処理の高速化

兎が周囲の草を探したり、狐が周囲の兎を探す処理がネックになってそうだったので修正しました。
元の処理は以下の通りです。

// 空腹なら近くの死んだウサギを優先的に探す
var deadRabbitFood = currentWorldState.agents
    .Where(a => a.type == AgentType.Rabbit && a.IsDead &&
                Vector3Distance(Position, a.position) < DeadRabbitSightRange) // 死んだウサギの探索範囲を拡大
    .OrderBy(a => Vector3Distance(Position, a.position))
    .FirstOrDefault();
private float Vector3Distance(SimpleVector3 v1, SimpleVector3 v2)
{
    float dx = v1.x - v2.x;
    float dy = v1.y - v2.y;
    float dz = v1.z - v2.z;
    return (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
}

現状では複数回Vector3Distanceが呼ばれています。
Vector3Distance(Position, a.position) < DeadRabbitSightRangeという判定では範囲外のエージェントとの距離も計算してしまいます。
またこの距離計算も平方根が使われていたり計算量が多いです。

なので以下のように修正しました。

// 空腹なら近くの死んだウサギを優先的に探す
var deadRabbitFood = currentWorldState.agents
    .Where(a => 
        a.type == AgentType.Rabbit &&
        a.IsDead &&
        Position.IsInRange(a.position, DeadRabbitSightRange)
        ) // 死んだウサギの探索範囲を拡大
    .OrderBy(a => Position.ManhattanDistance(a.position))
    .FirstOrDefault();
public struct SimpleVector3 : IEquatable<SimpleVector3>
{
    ...

    // 範囲判定メソッド(各軸ごとに±distance以内)
    public bool IsInRange(SimpleVector3 center, int distance)
    {
        return Math.Abs(x - center.x) <= distance
            && Math.Abs(y - center.y) <= distance
            && Math.Abs(z - center.z) <= distance;
    }

    public int ManhattanDistance(SimpleVector3 other)
    {
        return Math.Abs(x - other.x)
             + Math.Abs(y - other.y)
             + Math.Abs(z - other.z);
    }
}

周囲に存在するかの判定はInRangeにし、多少の計算と比較に済ませました。
距離計算では厳密性を捨てマンハッタン距離にしました。
一番近くのエージェントを探索するなら厳密性は必要なく、それなりに近い個体なら良いと判断しました。

これらの改善によりFPS5→FPS100ほどに上がったように感じました。

本番ではFPS1で動かしそれを観察する予定なのですが、シミュレーションの継続性を検証するために高速化し検証しました。

継続性の検証

シミュレーションログを残すようにして、何度か放置してみました。

しかし、2万フレームほどすると0xc0000409エラーで強制終了してしまいました。
単一スレッドに変更するとでなくなったので、スレッド数を増やし過ぎたのが原因だったようです。
スレッド数を半分にしたら発生しなくなりました。

その後何度か実行してみたところ、7万フレームほどが限界でした。

出生確立や寿命、フィールド範囲を変えただけでも大きな影響があり、確率が0.05変わっただけでもループに入らず数100フレームで絶滅ということが相次いでいました。

やはり自然発生や個体数制限のない無法フィールドでは、循環性を持たせるだけでも大変だということがわかりました。

とりあえずFPS1で動かせば約20時間ほど動くことが分かったので進めていきます。

ビジュアライザーの作成

HTTPリクエストから帰ってきたJSONの内容を表示させるWebページを作成します。
やはりC#の方が慣れているので、ASP .NETで作成していきます。

フレームワークはASP .NET CoreのBlazor WebAssemblyを使用した、シングルページアプリケーション(SPA)を作成します。
Canvasなどの一部機能はJavaScriptのAPIを使用するため、JavaScript Interopも使用していきます。
【2025年最新】Blazorとは?初心者にもわかりやすく解説|.NETで始める次世代Web開発 #C# – Qiita

シミュレーターと同時に実行すると、毎秒各エージェントが移動したり、繁殖したり、死亡したり、さまざまな行動を見ることができます。

シミュレーションを眺めていると、ある程度の法則性などを感じ取ることができました。
動画内でも、捕食者のもっとも単純な戦略は、腐肉食動物として行動すること、と紹介されています。
実際に狐は兎を追いかけるより、死体を回る行動をとっていました。
単純に処理の実行順からそうなったのですが、循環シミュレーションを安定させる一因にはなっていると思います。

また、植物にアクセスできない兎は大量の食べ物として狐に簡単に狙われることになる、などもありました。

EC2インスタンス

それではシミュレーションが動くようになったのでEC2インスタンスで実行していきます。

まずインスタンスを用意します。
今回はインスタンスタイプをt3.micro、OSをAmazon Linuxにしました。
自分は間違えてアメリカのリージョンに作成してしまいました。通信コストなどの問題から適切に選びましょう。

続いて、環境設定としてEC2にSSH接続します。
この時インスタンスを立てるときに取得したプライベートキーが必要になってきます。

.NET環境をセットアップします。
ビルド時に--self-contained trueを追加することで実行環境が不要になりますが、今回はEC2内に環境をセットアップしました。
実行環境に.NET Core ランタイムが不要なアプリをビルドする(自己完結型の展開)& Docker 上の Alpine Linux で動かす #C# – Qiita

続いて、ビルドしたプログラムを転送します。
はじめはSCPコマンドを使用していましたが、転送時間が長かったのでrsyncコマンドを使うことにしました。

Windowsではrsyncコマンドは存在しないようで、もしやる場合はGit Bashでやると良いとのことでした。
私がGitをインストールしたのが2020年以前だったせいかコマンドが存在してなかったので、Gitを再インストールしました。
数分近くかかっていたデータ転送が数秒になりました。

これで実行できるようになったので、実行してみました。

これでシミュレーションが実行され、HTTPリクエストを送るとJSONが返ってくるようになりました。
クライアント側の接続先をhttp://<EC2のパブリックIPアドレス>/api/worldに変更しました。

このままではSSH接続を切った時にプログラムも止まってしまうため、systemctlを設定します。
sudo nano /etc/systemd/system/my-simulation.serviceで設定ファイルを作成します。

その後、サービスの自動起動を有効化します。
sudo systemctl enable my-simulation.service

これでEC2での常時シミュレーションが完成しました。

データ分析

続いて個体数についてのデータ分析を行っていきます。
ログ内容を時系列のグラフにして特性などを探っていきます。

以下のグラフが、ある一定期間の個体数を記録した物です。
緑が草で、青が兎、赤が狐です。

単純に草と兎の関係を見てみると、上記のロトカ・ヴォルテラ方程式のような関係が見えてきますね。
草が増える→兎が増える
兎が増える→草が減る
草が減る→兎が減る
兎が減る→草が増える

その関係に加えて、狐も加わっています。
狐に関しては出生率が低く、寿命も長いため、単純にはグラフに表れにくいです。
ですが、狐が少ない時は兎が増えている様子が見えますね。

この関係において狐の重要性はあまり感じられませんが、実際に狐が絶滅すると結果的に全エージェントが絶滅してしまいます。
以下が狐が絶滅した後のグラフです。

循環的な動きをしていると思ったら、2000フレームほどで絶滅してしまいました。
狐と草に直接的な関係はなくても、実は絶滅を防ぐことにつながっていたんですね。

コスト

EC2のt3.microインスタンスはコストがかからないと思っていたのですが、いつの間にか料金が発生していました。

2~3日ほど動かして1.5ドルほどですね。
このコストの種類が、SSH接続の転送費用か、シミュレーションの計算費用か、HTTPリクエストの接続費用なのかでサービスの設計が変わってきますね。

おわりに

自分が想定していた24時間駆動のシミュレーションには至りませんでしたが、外部サーバーでシミュレーションするという目標は達成できました。
またその結果をHTTPリクエストで返すという、パブリックIPを持っていることの利点も活かせました。

シミュレーションでは、単純な捕食関係・出生・周辺探索処理だけでこの循環的な関係が作れることもよかったですね。
パラメータ調整は大変でしたが、細かな法則性が感じ取れて楽しかったです。

言語仕様をC#にしたのも良かったです。
ボトルネックとなっている処理を自分で改善出来たり、言語仕様が分かっているため改善の方向性が掴めました。
慣れない言語で作成していたら、改善も出来ず、ただ作って終わりだったでしょう。

以上、EC2を使った個人学習でした。
これでAWSのクレジットがもらえたので、また他のサービスも試してみたいですね。

投稿者プロフィール

MuramatsuYuki

関連記事

  1. AWS

    【AWS】AWS SAMを使いCLIでDynamoDBやLambda関…

  2. 【新米エンジニア学習記録③】Next.jsのデプロイ

  3. AWS

    【AWS】Amazon DynamoDBを使ってみる(CLI、Part…

  4. AWS

    【API Gateway】Lambda Web AdapterからWe…

  5. AWS

    【AWS】AWS Step Functions with Lambda…

  6. AWS

    【AWS】AWS SAMを使いCLIでLambda関数をデプロイ(Ty…

最近の記事

  1. AWS
  2. AWS

制作実績一覧

  1. Checkeys