simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

レイヤー構造AIデザイン(古いAI技術を新しいものに置き換えるのではなく、積み重ねて利用する考え方)

ふと、別部署の方とAI談義をする機会があったのですが、興味深い気づきを与えてもらいました。
まとめます。

ゲームAI技術のうち

GA と呼ばれる遺伝的アルゴリズムによる進化
ユーティリティーベースの習性による行動優先度決め
未来予測と行動選択をするモンテカルロツリーサーチ
DNNベースの特徴学習と特徴抽出結果の利用による判別や行動決定

などがありますが、これらを別々のものととらえるのではなく、ハードウェア寄り、ソフトウェア寄りにレイヤーを組んで、一式すべてを利用するエージェントを作ると、期待するAIに近づくだろうという話に発展しました。

陸上で速く走るために、意思の力で馬は指の爪を大きくして、つま先だけで走れるようになったわけではなく
海の中を効率的に速く泳ぐために、意思の力で鼻の穴を頭の上に移動させて、手をひれのようにし、毛をうろこ状に変化させたわけではなく
世代を超えて遺伝しつつも突然変異として多様性を持たせ、より優れた適応性を持つものを多く生き残らせることで、環境に適したフォルムを手に入れる
これが GA であり、GA は世代間の種の保存を目的としたハードウェアの構成要素としての AI と考えることができます。

例えば髪の毛を意識の力で伸ばしたり太くしたりはできません。
しかし、かみそりで剃るなどの刺激を与えると早く成長したり、太く強くなったりします。
これはハードウェア寄りのユーティリティーベースの習性によるもので
より健康で長生きできたために、そのようなハードウェア寄りのベースロジックが備わっていると考えます。

息苦しくなったら窓を開け、疲れたら眠り、腹がすいたら食事をし、種を残す行為をしたがる性欲がわくといったことになります。
これはソフトウェア寄りのユーティリティーベースの習性によるもので、ここで初めて意思の力でこうした行動を抑制できるようになりますが、それは置いておいて
ひとまずソフトウェア寄りの習性を持たせることで、こうした行動を行う動物的なAIを表現できます。

未来予測と行動選択により、死に関わるような一瞬の判断など、生き残るために重要な思考を行うことができます。
これはソフトウェアの機能としてのAIで、より正確に未来を予見できるものは頭が良いとされます。
ソフトウェア寄りの習性も、この未来予測と行動選択によって抑制することができ、特に動物の中でも、人はこの力を持つことによって、我慢と協調という社会性を獲得し
時に個としての種の保存よりも、種全体の保存にとって重要な行動を未来予測によって決定して死を迎える人が存在します。
習性としてこれを持つ蜂などの昆虫もいます。レイヤー間では、高いレイヤーから低いレイヤーへ習性として、なじむように作られていると考えられます。

DNNベースの特徴学習は近年注目されているAI技術ですが、これは未来予測を有事の際に高速に正確に行えるために、事前に最適化したモデルを獲得する技術ととらえることができます。
未来予測と実際の行動結果を近づけるために限られたリソース内で簡略化したモデルを構築し、これをソフトウェア機能としてのAIで利用します。
達人と呼ばれる領域になれば、レントゲン写真から腫瘍を発見したり、刀身の動きから相手の次の太刀筋を見極めることができるようになります。

私たち生命の動きを決めているAI要素はレイヤー構造となっていて

DNN(ディープラーニング:現在2018年に人類が挑んでいる最先端技術)
MCTS(モンテカルロツリーサーチ:AlphaGoなどの人類を超越した思考AI)
BT(ビヘイビアツリー:モダンゲームAI、ユーティリティベースのAIデザイン)
GA(遺伝的アルゴリズム:基本AI、ランダムに変化させて、良かったものを残す単純ロジック)

という順番でレイヤーを構成して、それぞれ下からハードウェア寄り、上に行くにつれてソフトウェア寄りにアルゴリズムやロジックを配置して走らせることで、生命として、意識を持つAIとして機能し始めることが初めてできるのではないか

なーんて、飲み会の後の酔った頭でバスに揺られながら1時間ほど話をしてました。
最近はAIを作るには未来予測が重要だと示していましたが、もちろんそれは変わりませんが、既存のGAや最新のDNNなども、未来予測を挟み込むレイヤー技術と考えることができるようになれたのが大きいです。
このレイヤー型技術を真剣に取り入れていけば、環境に適用するように世代ごとにだんだん進化していき、環境に適した習性をもつようになり、そのうえで習性を抑制するように直近の望む未来への行動を決定する AI が作れるようになると思うのです。

Distributed TensorFlow with Cloud ML Engine (Implementation Details)

■前置き
今現在、世界中の天才と呼ばれる人類層が、必死になって神秘のヴェールをはがしている領域で
ごく最近使われるようになってきた単語を使って説明するので、かなり知的負荷が高いこと書きます。
筆者もいっぱいいっぱいで、わからなくても気にせず、読み流してください。

■本記事で伝えたいこと
Deep Learning という言葉が流行り始めたのが 2012 年の ImageNet(別名Large Scale Visual Recognition Challenge)にてトロント大学チームがこれまでの画像認識率に大差をつけて勝利したころから。
世界中で人工知能 (AI) ブームが巻き起こり、Deep Learning ライブラリの群雄割拠時代が始まり、ようやく研究者の間で一つのライブラリに収束してきたこの頃です。
GoogleDeep Learning 用のライブラリ Tensor Flow と、膨大な計算量を簡易にスケールアウトできる Cloud ML サービスを提供しています。
Cloud ML は複数の計算マシンを活用して、本来自宅PCだと数週間かかるようなAIの計算を、その数百分の1の数時間で完了させる道具と認識できます。
世界で誰も見たことが無いような予測結果を描く Deep Learning を中心としたAI技術に携わる方は、Google の Cloud ML を最大限に活用しなければならないと言っても過言ではありません。(むしろここがスタートラインです)

Cloud ML と Tensor Flow の使い方ドキュメント
Introduction  |  TensorFlow
Distributed TensorFlow  |  TensorFlow
を順番に読んでいけば、Tensor Flow の Core API を利用した場合は、 あなたの実装コードに cluster (複数マシンの役割設定)定義を書く必要があることに気づくでしょう。
その具体的な記述方法と、なぜこんな書き方になるのか理解しなければならない概念イメージの中で特に集中して見えていなければならない点を強調します。
先ほどのリンク先を読み進めて、疑問点が晴れるのが一番良いので、深く理解できた方はこの先を読む必要はありません。
ここから先は、実装を試しながら深く読み進めた人間が、結局、少数の何を覚えておけばよいかという点を示していきます。

■具体的な記述方法

前提知識、技術水準はかなり高いかもしれません。
以下の項目ができるようになってから続きを読んでください。(ちょっと偉そうですね、すみません)

  1. Tensor Flow の Core API を使ってデータフローグラフのメンタルモデルを構築できる(要はコード見ただけで計算内容がイメージでき、図で他人に説明できる、別に他人は理解できなくて良い)
  2. 登場する Python 書式で不明点がない (または、不明書式が出てきてもすぐに調べて解決できるくらいのコンピュータ知識とプログラミング経験)

前提を揃えたら、Tensor Flow の Cloud ML 実行のチュートリアルが以下のリンクに示されているので、なぞります。
Using Distributed TensorFlow with Cloud ML Engine and Cloud Datalab  |  Cloud ML Engine for TensorFlow  |  Google Cloud
2018年7月に私が試した限りでは、問題なくすべてのチュートリアルが完了することを確認できました。

ここまでやった人なら気づきますが、コードレベルの詳細説明は載っていないので、このサンプルをベースに活用するには、利用した Python コードを読んで理解する必要があります。
コードをローカルに引きます。まずは次の git clone コマンドを成功させてください。

git clone https://github.com/GoogleCloudPlatform/cloudml-dist-mnist-example

PythonIDE として今おすすめなのは VSCode でしょうか、プラグインを入れて Python 実装を見やすくしておいてください。
キーワードとして cluster を検索します。
一つも引っかかりません。

ということで、これ Custom Estimator という Core API 使わない Tensor Flow のライトユーザー向けサンプルですね。

Tensor Flow 利用者は、新しく理論を作り出すような玄人向けの Core API を活用する人と、出来合いの Estimator をいじるライトユーザーの二つのユーザー層に分かれています。
今回のサンプルはライトユーザー層向けのものでした。

cluster の書き方は、結局
こちらDistributed TensorFlow  |  TensorFlow
を参照して、頑張って API ドキュメントの知識を繋ぎながら確信あるサンプルコードを自力で作る必要があります。

ということで、私が自力で作ってみるので、少々お待ちください。

追記:
とても参考になる実装を見つけました。
2017年10月と、半年以上古いですが Google の中の人のサンプルです。
github.com

こちらがグラフ作成時に気を付けることです。
あらかじめ cluster を作っておいたら、その情報を使って現在のマシンに関する文字列を作り、device_fn を作ります。
それを tf.device に渡して、その後のスコープでグラフを作成します。
これによって、グラフは指定されたマシンでのみ実行されることになります。

    if self.cluster:
      logging.info('Starting %s/%d', self.task.type, self.task.index)
      server = start_server(self.cluster, self.task)
      target = server.target
      device_fn = tf.train.replica_device_setter(
          ps_device='/job:ps',
          worker_device='/job:%s/task:%d' % (self.task.type, self.task.index),
          cluster=self.cluster)
      # We use a device_filter to limit the communication between this job
      # and the parameter servers, i.e., there is no need to directly
      # communicate with the other workers; attempting to do so can result
      # in reliability problems.
      device_filters = [
          '/job:ps',
          '/job:%s/task:%d' % (self.task.type, self.task.index)
      ]
      config = tf.ConfigProto(device_filters=device_filters)
    else:
      target = ''
      device_fn = ''
      config = None

    with tf.Graph().as_default() as graph:
      with tf.device(device_fn):
        # Build the training graph.
        self.tensors = self.model.build_train_graph(self.args.data_dir, self.args.batch_size)

気になる cluster の作成方法ですが、以下のように環境変数 TF_CONFIG から情報を取得して、マシン情報を取得し、処理を分岐します。

  env = json.loads(os.environ.get('TF_CONFIG', '{}'))

  # Print the job data as provided by the service.
  logging.info('Original job data: %s', env.get('job', {}))

  # First find out if there's a task value on the environment variable.
  # If there is none or it is empty define a default one.
  task_data = env.get('task', None) or {'type': 'master', 'index': 0}
  task = type('TaskSpec', (object,), task_data)

  cluster_data = env.get('cluster', None)
  cluster = tf.train.ClusterSpec(cluster_data) if cluster_data else None

クラウドで準備したマシンの環境変数 TF_CONFIG から、このように情報を取得しなさいというドキュメントは、こちらに書かれています。
Using TF_CONFIG for Distributed Training Details  |  Cloud ML Engine for TensorFlow  |  Google Cloud

最後に、ps ことパラメータサーバーだった場合は、グラフ構築を行う前に server.join を行って、他の master や worker からのリクエストを待つために join します。

def dispatch(args, model, cluster, task):
  if not cluster or not task or task.type == 'master':
    # Run locally.
    Trainer(args, model, cluster, task).run_training()
  elif task.type == 'ps':
    run_parameter_server(cluster, task)
  elif task.type == 'worker':
    Trainer(args, model, cluster, task).run_training()
  else:
    raise ValueError('invalid task_type %s' % (task.type,))


def run_parameter_server(cluster, task):
  logging.info('Starting parameter server %d', task.index)
  server = start_server(cluster, task)
  server.join()

def start_server(cluster, task):
  if not task.type:
    raise ValueError('--task_type must be specified.')
  if task.index is None:
    raise ValueError('--task_index must be specified.')

  # Create and start a server.
  return tf.train.Server(
      tf.train.ClusterSpec(cluster),
      protocol='grpc',
      job_name=task.type,
      task_index=task.index)

パラメーターサーバーは join するとして、他の master と worker は?
と思いますが、以下のように session を作る際に target として server を指定します。

server = start_server(self.cluster, self.task)
target = server.target
with self.sv.managed_session(target, config=config) as session:

このセッションを run すると、それぞれのマシンで計算が行われた時に、join しているパラメータサーバーが必要な変数情報を渡してくれるので、worker は単に計算を頑張り、結果をパラメータサーバーに返します。
以下の説明によれば、バッチ処理など、入力情報が異なるものについて計算を行い、変数の変更内容を互いにマージするように取り入れることによって、並列化による計算速度の向上をする仕組みとなっています。
Using Distributed TensorFlow with Cloud ML Engine and Cloud Datalab  |  Cloud ML Engine for TensorFlow  |  Google Cloud

Tensor Flow API についての知識をあらかたそろえてから Cloud ML のドキュメントを読み進めると、こうした Tensor Flow の記述についてわかってくるということでした。
Cloud ML は日本語訳が充実しているので、英語が苦手でも素早く読めます。

Concepts  |  Cloud ML Engine for TensorFlow  |  Google Cloud

TensorFlow GAN メモ

■前置き
今から半年ほど前の Google AI Blog の記事

ai.googleblog.com
TFGAN: A Lightweight Library for Generative Adversarial Networks

要約すると

「GANってのは、コンテンツ生成という新しい研究領域を広げてます。
例を見てみ、すごいっしょ!
すぐに動かせる軽量のチュートリアル作ったから、見てみ
めっちゃテストしたから、数学や確率のミスは無いと思って信用してツールとして使ってほしい
俺たちと同じ環境使って一緒にがんばろ!」

なるほど、これは面白そう!
前回、前々回と TensorFlow がGoogle Cloud ML で動くようになったし
とりあえずサンプル動くところまでやってみたくなりました。

そこでだ、実装について最初に読んだ解説がこちら
github.com
TensorFlow-GAN (TFGAN)

チュートリアルへの案内あり、こちらを先に読むことにした。
github.com

■作業ログ

まずは TensorFlow で gan が使えるか確認します。
python -c "import tensorflow.contrib.gan as tfgan"
エラーが出るようなら、出ないように TensorFlow のチュートリアルをこなしましょう。
一応エラー出なかった、使える!

Python 勉強
コード単体ならどんどん読み進められますが、ファイルをまたいで、not found エラーとかランタイムで出されると辛いっすよね。
この辺は TensorFlow とは別で Python の基本から頭に入れておかなければならない。
kannokanno.hatenablog.com

一言じゃ語れない
Python コード書式で不明点をなくす + Tensor Flow のデータフローグラフのメンタルモデルを作れるようにする
この二つの合わせ技がないと TensorFlow-GAN のコード読みの入り口に立てません。
無知から始めると、マスターまで三日くらい要しますので、連休とかに集中して取り組む覚悟が必要だと思います。

GAN の論文とか解説記事も理解できるようになる必要もあるので、結構頭使います。

もう私もいつのまにかオッサンなので、ゼロから知識構築するの大変

これが生きるってことなのか(◞‸◟)

Google Cloud Machine Learning Engine で分散ディープラーニング

■前置き
TensorFlow をクラウド環境で分散実行したい

公式のチュートリアルが分かりやすかったです。

本記事はGoogle Cloud ML って何?て状態の人が
そのチュートリアルにたどり着くまでの記録

■手順
参考サイト
www.topgate.co.jp

Google アカウントを作成
コンソールへ行く

Billing または お支払いから、プロジェクトに支払いアカウントを紐づける(今だと300ドルもらえるので、これを有効化するだけでいい)

Google の良いところは、プロジェクト単位で支払い方法を制御できる点とのこと
多数のプロジェクトを捌くエンジニアにとってうれしい

公式チュートリアル(英語)
Getting Started  |  Cloud ML Engine for TensorFlow  |  Google Cloud
1.ローカル単一学習
2.ローカル分散学習
3.クラウドストレージにバケット作成、学習データの配置
4.Cloud ML で単一学習
5.Cloud ML で分散学習
6.ハイパーパラメータ調整
7.Cloud ML 分散学習結果をモデルとして定義
8.モデルを使って単一データから予測
9.モデルを使って複数データからバッチ予測
10.クラウドストレージのバケットを削除

なぞるだけで TensorFlow を Cloud ML Engine で動かす操作が具体的にイメージできるようになります。

これができるようになってから
Cloud Machine Learning Engine のドキュメント  |  Cloud Machine Learning Engine(Cloud ML Engine)  |  Google Cloud

を読んでみることにしました。

…読みましたけど、あまり役立つ情報は書かれていなかった。南無…

TensorFlowの導入記録(エラー無かった)

■前置き
近頃はやりの Deep Learning を触ってみることにした。

Deep Learning はホントに流行っていて、覚えきれないくらいツールが溢れている(+_+)

未来があると思われる、Googleが開発したTensorflow(テンサーフロー)
の Hello, World なる MNIST の学習曲線を描いてみようと思います。

MNIST ってのは 0~9 の10個の手書き文字を識別する人工知能制作に挑む話
かなり最初の頃の Deep Learning の課題で、入力データとしてどの Deep Learning ツールも用意している。

詳しく知りたい人はこちらの論文どうぞ(そういえばどこぞの勉強会の前に読まされたな…)
http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf

Python 3 が必須

コマンドプロンプト (Win + cmd) を開いて、python とか py と打ち込めば、環境に python を入れていることがわかる

あ、入ってなかった。

python 3.6 入れます。
Anaconda for Windows のインストール

Anaconda prompt が入っているので、ここで python と打つと
Python 3.6.5 |Anaconda, Inc.|
と表示されればOK

■CUDA Toolkit 9.0 が必要

GeForce が刺さっているPCなので、ドライバを最新にしておく。
GeForce Game Ready Driver 397.93 入れた。

CUDA Toolkit 9.0 のインストーラを入手、インストール
必ず Visual Studio Integration に失敗するので、カスタムインストールで外す(なんで?失敗したやつだけスキップして進めなさいよ)

失敗を抜けて入った。(キレ気味)

■Tensorflow(テンサーフロー)のインストール

ここから公式ドキュメントなぞる
Installing TensorFlow on Windows  |  TensorFlow

最初からやっとけばよかった^^
Requirements to run TensorFlow with GPU support

環境変数の Path に CUDA 関係のことやるらしい。
CUDA_PATH も Path にも CUDA 9.0 のものがいっぱい入っていました。もうやらなくていい感じかな?

cuDNN は別途ダウンロードが必要とある、入れてみた。
これにはユーザー登録が必要…仕方ないので入れてみた。

cuDNN v7.1.4 Library for Windows 10 が CUDA 9.0 用の cuDNN っぽいね。
これを使うことにした。
Path も通してやった
C:\Program Files\NVIDIA GPU Computing Toolkit\cudnn-9.0-windows10-x64-v7.1\cuda\bin
こんなところに展開
確かに cudnn64_7.dll ファイルが存在している。

続いて pip ? を使う native で環境作るか、仮想環境作るか選ぶことになるらしい
既存の python 環境を壊すことあるけど、native ならどこのディレクトリからもコマンド呼べるのでいいよって勧めている

でも、Installing with Anaconda の方やってみることにする。

conda create -n tensorflow pip python=3.6
を実行
Solving environment: done と出た(^^

activate tensorflow

で(base)から(tensorflow)に切り替わることを確認
(tensorflow)C:> # Your prompt should change ドキュメントもそういっている。わかりずらい?

To install the GPU version of TensorFlow, enter the following command (on a single line):
ドキドキ、やってみます!

ゴリゴリテキストが進む、途中止まった?3分くらいでプロンプトが戻った。成功した模様\(^_^)/

Hello, TensorFlow!

できた、問題なくGPU版tensorflow動かせている
ここからチュートリアルを頑張れって導線が引かれた…やりますかぁ

オンラインゲーム化の準備

昨年9月からAIに身体性を与えるためのマイクロワールドの構築を続けてきました。
趣味プログラムなので、やりたいことをやりたい順番で進めてきたわけですが、見積もりをしてこなかったが故にしっぺ返しが…
さすがにブロック同士の相互作用を行う大気シミュレーションはマシンパワーに見合わないので、途方に暮れていたのです。
(計算を別スレッドに任せることでゲームは固まりませんが、人間が面白いと感じる速度で世界は変化していかないわけです…)

大気の循環は3次元で行わずに2次元マップで行うようにして、計算をさらに簡略化しようと思いますし

もう一つ、ここまでずっとクライアントサイドの実装を続けていましたが、時代の変化に合わせる形で
オンラインゲームのサーバーサイド技術も休日を使って勉強していかないといけなくなってしまいまして

勉強のモチベーションを高めるために、AIに身体性を与えるためのマイクロワールドをオンラインゲーム化するという名目で趣味プログラムを再開したいと思います。

と、書いておけば、そのうち記事を更新しだすと信じて、自分の身体に鞭打ってみます。

追記1:
あれ、実装途中のコードを見直してみると、物質の3態のベースを作るのに頭を悩ませていますね…3次元空間の相互作用を全然あきらめていないじゃん
計算負荷をクライアントやサーバーに分散する方向で世界を加速させる手段という方向で、オンラインゲームの勉強をしてみます。

AIに身体性を与えるためのマイクロワールドの構築18:大気の循環

この世界で水が山頂の方から流れてくるのはなぜか考えたことはありますでしょうか?
大気は循環しており、その大気に水が含まれているため、その大気が雨を降らせ、山から水が流れてきます。

f:id:simplestar_tech:20180204232847j:plain

はい、ということで今回は大気の循環をブロックのロジックを使って再現したいと思います。

大気の流れというものを、太陽からの輻射熱、地表の温度上昇、熱の伝播、暖気は冷気より軽い、重い空気は落下し…大気は流れる、というロジックで動かしてみたいと思います。

細かく見ていくと気体分子の運動量の伝播なのですが、そこまで細かくは作れませんので、具体的には
・太陽光と地表面のなす角より輻射熱の吸収、放出量が変化
・地表ブロックの判定
・地表ブロックの熱量増加
・地表ブロックに触れている空気へ熱が移動
・温まった空気ブロックが水分を水ブロックから吸収
・暖気の上に冷気がある場合は上下ブロックの内容を交換
というロジックで動かそうと思います。
これで空気が移動して風が吹き、さらに空気が水分を含むように作ることで大気と水の循環が行われる世界が出来上がる…はずです。

実装して確かめてみましょう。

・太陽光と地表面のなす角より輻射熱の吸収、放出量が変化

いまだ存在していなかった時間という概念をWorldに与えます。
続いて、その時間をもとに太陽の角度を決定します。
一日を 0~2π の値で表すと次の通り

worldTimeDayRadian = (2.0f * (float)Math.PI) * (Instance.worldTime.Hour / 24.0f + Instance.worldTime.Minute / (24.0f * 60.0f));

この値は太陽の角度決定にそのまま使い、さらに次のように調整することで
_radiantHeat = (float)Math.PI - Math.Abs(Instance.worldTimeDayRadian - (float)Math.PI)
輻射熱の吸収、放出量の係数として利用します。

そのほか、北と南、東と西の概念を世界に与えます。
チャンクごとに緯度と経度がありますので、世界標準時の経度と日付変更線、そして北と南の緯度における太陽の角度、日照時間の短さです。
年間を通して日照時間は変化するようにし、あたかも世界が球体で自転の軸が23.4度ほど傾いているように見せかけます。

・地表ブロックの判定

太陽の角度については特に考えず、上空から探索して最初に不透明ブロックを見つけた場合に、これを地表ブロックとします。(半透明の雲や水面は透過率をもって通過します)
ブロックのロジックに地表としての処理用インタフェースを追加して、これを今までのブロックのWorkを行っているタイミングと一緒に呼び出すこととします。

コードのイメージはこんな感じです。

BlockDataSt blockData = BlockDataSt.AirBlock;
for (int widthIndex = 0; widthIndex < Width; widthIndex++)
{
    offsetWidth = Depth * Height * widthIndex;
    for (int depthIndex = 0; depthIndex < Depth; depthIndex++)
    {
        offsetDepth = Height * depthIndex;
        bool isSurface = false;
        for (int heightIndex = Height - 1; 0 <= heightIndex; heightIndex--)
        {
            blockData.data = localBlockData[offsetWidth + offsetDepth + heightIndex];

            if (!isSurface && MaterialID.Material_Translucent < blockData.materialId)
            {
                if (MaterialID.Material_Opacity < blockData.materialId)
                    isSurface = true;

                blockData = blockLogics[(uint)blockData.materialId]?.SurfaceWork(blockData, worldUtility) ?? blockData;
            }

            BlockDataSt resultBlockData = blockLogics[(uint)blockData.materialId]?.Work(blockData, widthIndex, depthIndex, heightIndex, localBlockData, worldUtility) ?? blockData;
            localBlockData[offsetWidth + offsetDepth + heightIndex] = resultBlockData.data;
・地表ブロックの熱量増加

先ほど追加した SurfaceWork 関数を実装して、輻射熱係数をたよりに heat を上昇させたり、下降させたりしてみます。
テストとして、最高温度に達したら空気ブロックにマテリアル変換を起こすロジックを作ってみました。すると…

f:id:simplestar_tech:20180204231816g:plain

こんな感じで昼の間に表面の土ブロックだけが消失している絵ができました。
動画はこちら

www.youtube.com

今のままだと太陽光を受け、熱が上昇し続け、あっというまに最高温度に到達するわけですが、これは物理現象としてあり得ません。(地上で生きていけない…)
本来物質は固体の状態で熱が上がると、自身もその熱に応じて輻射熱を周囲へ放射します。
熱が上がれば輻射熱量もどんどん高くなるため、ある程度熱くなってくると、なかなか熱くならなくなるわけです。

しかし、残念ながら現在の計算スピードを考えると、周囲ブロックへ放射熱が伝播するところまで計算していられないので、ここは乱数に頼ってみようと思います。
低温状態の地表ブロックに太陽からの輻射熱を与えると高い確率で地表ブロックの熱が上昇しますが、熱量が高まるにつれて上昇の確率が下がっていくという仕組みを導入します。

熱量が上昇すると、徐々に熱の上昇確率が減っていき、例えば以下のようなグラフの曲線はどうでしょうか?
f:id:simplestar_tech:20180210193928j:plain
こちらは 0.98 を熱量の値で累乗した結果です。

最初は 0 乗なので 100%, 255の値付近では 0.5% です。
チャンクのブロック処理の頻度はそこまで高くないため、だいたい100の値を越えたあたりで、ほぼ熱量の上昇はしないという形になります。

次に太陽光を受け取らなくなった時に、徐々に放熱しなければなりませんので、これについては次のように、高い温度だった場合に周囲に高い熱量を放射する…

f:id:simplestar_tech:20180210195245j:plain

いや、これだけで高い温度に到達しずらい仕組みが作れそうですね。
ある角度で太陽光が当たったなら、その角度に応じた熱がブロックに加算され、比熱の値がブロックのマテリアルごとに決まっていて、その値で割った値が上昇する温度
しかし、温度に応じで周囲へ放射する量が決まっていて、太陽光の放射量がそれを越えない限りそこからの温度上昇は起きないという形です。
これなら太陽の力が強い時はしっかりとそれに応じた地表温度というものに収束します。(漸近)

テストシーンにこの仕組みを適用すれば、ブロックは地上から干上がることはなくなりました。

最終的に落ち着いたアイディアとして、太陽の日照密度というものを 0~1 の値で表現するようにして、単純に日の出の瞬間に0から始まり sin 波として太陽が直上に来た時に 1.0、日が沈むころに 0 となり、夜間はすべて 0 となる値を地表ブロックは受け取れるようにしました。
そこに比熱となる要素をかけることにします。
この比熱の値は light の値の範囲に気を付けないと、永遠に地表は温まらないことになるので、後程調整が必要です。(byte で温度表現のつらいところですね)

public virtual BlockDataSt SurfaceWork(BlockDataSt blockData, ref float light, IHexWorldUtility worldUtility)
{
    if (1.0f > lightTransmittance)
    {
        float addHeat = ((1.0f - lightTransmittance) * light) / specificHeat;
        float nextHeat = blockData.heat + addHeat;
        float radiantHeat = (float)Math.Pow(radiantHeatBase, nextHeat);
        blockData.heat = (byte)Math.Min(Math.Max(nextHeat - radiantHeat, 0), byte.MaxValue);
        light = lightTransmittance * light;
    }
    return blockData;
}

今後、シミュレーション結果を通じて、各ブロックの放射熱量、比熱の全体スケールというものを調整していきたいと思います。

・地表ブロックに触れている空気へ熱が移動

まず、私は気象学について無知でしたので次の入門記述を読みました。
気象学超入門

要はここに書いてある、上昇気流が低温化して雲を発生させるメカニズムというものを SimpleHexWorld (マイクロワールド)で実現しようとしています。

これまでの構想を経て、現在空気ブロックには熱と圧力の値が byte で詰められています。
この二つの値のうち、今は熱を動かそうとしていますが、まずは空気ブロックに標高に応じた圧力の値を詰めるべきと考えました。

単純に最も上空の空気は熱 0 / 255, 圧力 0 / 255 という宇宙(space)ブロックで固定し、宇宙に熱(分子の運動量)を逃がし続ける存在とします。
初期値としては地上に向かって降りるにしたがって圧力を 1 ずつ増やしていき、チャンクの最深部に到達する頃には圧力が 255 / 255 になる形で初期値を打ちます。

圧力の伝播も熱の伝播と同様にベースとなるロジックを書く必要があると思いました。

では、次に熱の伝導の部分を詰めていきましょう。
熱の伝導において、与える熱量と温まりやすさという度合いが重要になります。(熱伝導量と比熱ですね)
空気ブロックに、乾燥断熱減率の概念を導入しようと思います。
圧力が低くなるにつれて、heat が一定の割合で低くなっていくという現象です。(開放系の気体の振る舞いですが、圧力が異なる気体の中の分子の動きを脳内でイメージするの、難しいんですよね)

上空は低密度ですので、単純に単位ブロックあたりの質量は小さくなります。
大きい質量と小さい質量の気体を重力場に置けば、流動結果として大きい質量の方が重力方向の先に移動します。
地球に住む人間はこれを上昇ととらえているみたいですね。

上昇下降については単位ブロックあたりの質量を見て、大きい方を常に重力方向に配置するように動かせばよいのです。

太陽の光によって、だいたい地表の温度は 0~255 の値を行き来します。
自身を含めた周囲の温度の平均を自身の温度にするようにしてみます。
まずはこれで地表付近の空気ブロックから温まり始め、徐々に熱は上空に移動し始めます。
空気は放射する計算を行わせていないので、夜になると地表より空気の方が暖かくなり、地面によって空気は冷やされることになります。

夜は上空の大気の方が暖かくなる可能性があります。
もちろん地上も空気の熱伝導によって温まりますが、空気と同じように温度を混ぜ合わせることはできません。
例えば、土ブロックの周囲に空気ブロックがある場合、周囲の空気ブロックが 100 ℃だったとして、土ブロックが0℃、このとき周囲8ブロック+自身1ブロックの平均88℃まで土ブロックは上昇しません。
温まりやすさというものがあったとして、…つまり比熱ですね。

このブロックの heat の値を絶対にするか相対にするかで悩みましたが、絶対にすると熱伝導の高低の判別計算が楽ですので、絶対にします。(世界の熱分解能は低いですね…)

分解能の低さは丸め誤差による情報の欠損です。あまりにもエネルギーが保存されない世界はそれはそれで作る意味がありません。
そこで!
熱の移動について、ここに確率を取り入れてみるのはどうでしょうか?
比熱比で確率計算する感じで、具体的には、ブロックの周囲のheat値を確認し、もし自分が高い場合は、相手に熱を +1 する、しかしそこに確率が生じていて
土から空気の場合は比熱比が1以上になるので確実に +1 、つまり 100% 伝導し、移動した熱量だけ自分も -1 するべきところ、しかし、そこに確率が生じていて、比熱比が 0.01 となっているので1%で自身の heat を-1します。
次に、もし自分が低い場合は、空気ブロックの heat を -1 して、土のブロックの heat を +1 しますが、これも先ほどの確率計算で 100% と 1% の値を使って、周囲の空気は冷えやすく、土ブロックは温まりづらいという現象が実現でき、さらに丸め誤差による情報欠損が多少なり改善されます。(この低分解能な世界に必要な表現です、なかなか良いアイディアを思いついたと思っています。)
先ほどの空気と土が互いに88℃になる現象は、100%と1% の確率を使えば、期待値として7.4℃になるわけです。(土ブロックは空気ブロック100個分の熱エネルギーを蓄えられると考える)

こういう話、密度と比重と体積について、みなさん勉強してきたと思います。
ですが、このマイクロワールドにおいて、体積は固定されたブロックの単位でのみ存在しますので、考えるべきは密度と比重をかけた値、つまりブロックの質量のみを考えるだけでうまくいきます。

また、熱伝導率という概念を導入して、先ほど +1 ずつ値を変化させましたが、高い熱伝導率を持つような金属の場合はもっと高速に heat のやり取りを行うべきです。(温度差が 1 なのに 10も移動してはおかしいので、そういう場合は比熱比を確認して 確率を上昇させてから +1 にするなどですね、これもいいアイディアを思いついたと思いました。)

整理するとマテリアルのロジックは次の定数を持つべきということです。

public float specificHeat { get; protected set; }
public float lightTransmittance { get; protected set; }
public float radiantHeatBase { get; protected set; }
public float thermalConductivity { get; protected set; }

また実装で詰まってきた…チャンクを越えられない…
チャンク内はスレッド内のローカル変数を書き換えるだけなので問題ありませんが、チャンクをまたいだ編集を行おうとすると他のスレッドからアクセスされないようにロックする必要が…
これではまたパフォーマンスが落ちてしまうので、悩みます。

一つアイディアが沸いたのですが、スレッド内のローカル変数にチャンクに接している周辺8つのチャンクのためのローカル変数を用意しておき
仕事中はスレッド内の変数を更新するのみとして、処理後にロックが必要ならロックして書き換えるというのはどうでしょうか?

これもパフォーマンスは悪くなりそうですが、仕方ないですよね。
作ってみます。(処理が重くなるのこえぇ)

デバッグ風景
f:id:simplestar_tech:20180212104833j:plain

先ほどのアイディアを具体的にすると次の通り(このポッと具体化するのが大変なんですよね…頭使う、まぁ問題を解決するために手段を具体化して簡単な作業の組み合わせに変えるのが仕事なんですけどね)

lock (LockObject)
{
    if (null != BlockData)
    {
        for (int copyWidthIndex = 0; copyWidthIndex < Width; copyWidthIndex++)
        {
            Array.Copy(BlockData, Depth * Height * copyWidthIndex, localBlockData, (Depth + 2) * Height * (copyWidthIndex + 1) + Height, Depth * Height);
        }
    }
}

{
    Chunk chunk_mm = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, -1)][World.Instance.RemapLatitude(PositionZ, -1)];
    lock (chunk_mm.LockObject)
    {
        if (null != chunk_mm.BlockData)
        {
            Array.Copy(chunk_mm.BlockData, Width * Depth * Height - Height, localBlockData, 0, Height);
        }
    }
}
{
    Chunk chunk_pm = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, +1)][World.Instance.RemapLatitude(PositionZ, -1)];
    lock (chunk_pm.LockObject)
    {
        if (null != chunk_pm.BlockData)
        {
            Array.Copy(chunk_pm.BlockData, (Depth - 1) * Height, localBlockData, (Width + 1) * (Depth + 2) * Height, Height);
        }
    }
}
{
    Chunk chunk_pp = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, +1)][World.Instance.RemapLatitude(PositionZ, +1)];
    lock (chunk_pp.LockObject)
    {
        if (null != chunk_pp.BlockData)
        {
            Array.Copy(chunk_pp.BlockData, 0, localBlockData, (Width + 2) * (Depth + 2) * Height - Height, Height);
        }
    }
}
{
    Chunk chunk_mp = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, -1)][World.Instance.RemapLatitude(PositionZ, +1)];
    lock (chunk_mp.LockObject)
    {
        if (null != chunk_mp.BlockData)
        {
            Array.Copy(chunk_mp.BlockData, (Width - 1) * Depth * Height, localBlockData, (Depth + 1) * Height, Height);
        }
    }
}

{
    Chunk chunk_zm = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, 0)][World.Instance.RemapLatitude(PositionZ, -1)];
    lock (chunk_zm.LockObject)
    {
        if (null != chunk_zm.BlockData)
        {
            for (int copyWidthIndex = 1; copyWidthIndex <= Width; copyWidthIndex++)
            {
                Array.Copy(chunk_zm.BlockData, Depth * Height * copyWidthIndex - Height, localBlockData, (Depth + 2) * Height + ((Depth + 2) * Height * (copyWidthIndex - 1)), Height);
            }
        }
    }
}

{
    Chunk chunk_zp = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, 0)][World.Instance.RemapLatitude(PositionZ, 1)];
    lock (chunk_zp.LockObject)
    {
        if (null != chunk_zp.BlockData)
        {
            for (int copyWidthIndex = 1; copyWidthIndex <= Width; copyWidthIndex++)
            {
                Array.Copy(chunk_zp.BlockData, (Depth * Height) * (copyWidthIndex - 1), localBlockData, (Depth + 2) * Height * (copyWidthIndex + 1) - Height, Height);
            }
        }
    }
}

{
    Chunk chunk_mz = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, -1)][World.Instance.RemapLatitude(PositionZ, 0)];
    lock (chunk_mz.LockObject)
    {
        if (null != chunk_mz.BlockData)
        {
            Array.Copy(chunk_mz.BlockData, (Depth * Height) * (Width - 1), localBlockData, Height, Depth * Height);
        }
    }
}

{
    Chunk chunk_pz = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, +1)][World.Instance.RemapLatitude(PositionZ, 0)];
    lock (chunk_pz.LockObject)
    {
        if (null != chunk_pz.BlockData)
        {
            Array.Copy(chunk_pz.BlockData, 0, localBlockData, (Depth + 2) * Height * (Width + 1) + Height, Depth * Height);
        }
    }
}
lock (LockObject)
{
    if (null != BlockData)
    {
        for (int copyWidthIndex = 0; copyWidthIndex < Width; copyWidthIndex++)
        {
            Array.Copy(localBlockData, (Depth + 2) * Height * (copyWidthIndex + 1) + Height, BlockData, Depth * Height * copyWidthIndex, Depth * Height);
        }
    }
}

では、テストとしてあるブロックを用意します。
見た目で分かるようにオレンジ色のブロックにしました。

ロジックは次の通り、周囲ブロックを自分と置き換える増殖ロジックとしました。

System.Random rand = HexWorldUtility.RandomProvider.GetThreadRandom();
if (1 == rand.Next(0, 30))
{
    BlockDataSt localBlock = BlockDataSt.AirBlock;
    for (int sideIndex = 0; sideIndex < 6; sideIndex++)
    {
        int sideWidthIndex = widthIndex;
        int sideDepthIndex = depthIndex;
        worldUtility.GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);

        for (int nextHeight = 0; nextHeight < Chunk.Height; nextHeight++)
        {
            localBlock.data = localBlockData[sideWidthIndex * (Chunk.Depth + 2) * Chunk.Height + sideDepthIndex * Chunk.Height + nextHeight];
            if (MaterialID.Material_Opacity > localBlock.materialId)
            {
                localBlockData[sideWidthIndex * (Chunk.Depth + 2) * Chunk.Height + sideDepthIndex * Chunk.Height + nextHeight - 1] = blockData.data;
                break;
            }
        }
    }
    blockData.materialId = MaterialID.Earth;
}
return blockData;

結果がこちら
f:id:simplestar_tech:20180212130543j:plain
うわ、増えていってます。
確認したかったのはチャンクの境で情報が遮断されずに、しっかりと情報が届き、処理されていくかという点です。

最後にテストとして、ブロックがある方角へ進み続けるというロジックを組んでみます。
走り出しました。(消えることがあったので、やはりチャンクの境で他の処理と重なってブロックが消されることはありそうですね)

さて、メッシュの更新フラグの立て方について調整が必要なことがわかりました。
自身のマテリアルIDが変わったときを、これまでトリガーとしていましたが、周囲のマテリアルIDが変わったときにもそれを通知する必要があるためです。

これについてはロジック内の現象なので、外部からいちいちサポートできませんが、ユーティリティ関数を用意して、ローカルブロック情報のどこを変更したかを記録することとします。
しかし、これもスレッドごとにまとまってしょりしなければならない値なので、周囲8ブロック + 自身 1ブロック分のレイヤー更新フラグを所持します。

デバッグを続けて、ひとまず形になりました。

www.youtube.com

話は戻って熱伝導ですが、ブロックのロジックから他のブロックのロジックを手に入れることができないので、できる仕組みを考えて作ります。

素案としては、こんな感じ

protected void HeatConduction(int widthIndex, int depthIndex, int heightIndex, ulong[] chunkBlockData, BlockDataSt blockData, IHexWorldUtility worldUtility)
{
    float removeHeat = 0;
    BlockDataSt localData = BlockDataSt.AirBlock;
    for (int heightOffset = 1; heightOffset >= -1; heightOffset--)
    {
        if (0 == heightOffset)
            continue;
        int topBottomBlockIndex = GetDataArrayIndex(widthIndex, depthIndex, heightIndex + heightOffset);
        localData.data = chunkBlockData[topBottomBlockIndex];
        _MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
        chunkBlockData[topBottomBlockIndex] = localData.data;
    }

    for (int sideIndex = 0; sideIndex < 6; sideIndex++)
    {
        int sideWidthIndex = widthIndex;
        int sideDepthIndex = depthIndex;
        GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);
        int sideBlockIndex = GetDataArrayIndex(sideWidthIndex, sideDepthIndex, heightIndex);
        localData.data = chunkBlockData[sideBlockIndex];
        _MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
        chunkBlockData[sideBlockIndex] = localData.data;
    }

    float subHeat = Math.Max(removeHeat, 1);
    Random random = worldUtility.GetThreadRandom();
    if (removeHeat > random.NextDouble())
        blockData.heat = (byte)Math.Max(blockData.heat - subHeat, 0);
}

private void _MoveHeat(ref BlockDataSt blockData, ref BlockDataSt targetBlockData, ref float removeHeat, ref IHexWorldUtility worldUtility)
{
    if (blockData.heat > targetBlockData.heat)
    {
        BaseBlockLogic targetLogic = worldUtility.GetBaseLogic(targetBlockData.materialId);
        if (null != targetLogic)
        {
            float addHeat = Math.Min(targetLogic.thermalConductivity, blockData.heat - targetBlockData.heat);
            targetBlockData.heat = (byte)Math.Min(targetBlockData.heat + addHeat, byte.MaxValue);
            removeHeat += addHeat * (targetLogic.heatUpEnergy / heatUpEnergy);
        }
    }
}
private void _SetLayerUpdatedFlag(bool[][] layerUpdatedFlags, int sideIndex, int heightIndex)
{
    for (int heightOffset = -1; heightOffset < 2; heightOffset++)
    {
        layerUpdatedFlags[sideIndex][Math.Min(Math.Max((heightIndex + heightOffset) / Chunk.LayerHeight, 0), Chunk.LayerCount - 1)] = true;
    }
}

それから色々と調整して、次の形にしました。

protected BlockDataSt HeatConduction(int widthIndex, int depthIndex, int heightIndex, ulong[] chunkBlockData, BlockDataSt blockData, IHexWorldUtility worldUtility)
{
    Random random = worldUtility.GetThreadRandom();
    float removeHeat = 0;
    BlockDataSt localData = BlockDataSt.AirBlock;
    for (int heightOffset = -1; heightOffset <= 1; heightOffset++)
    {
        if (0 == heightOffset/* || 1 == random.Next(0, 2)*/)
            continue;
        int topBottomBlockIndex = GetDataArrayIndex(widthIndex, depthIndex, heightIndex + heightOffset);
        localData.data = chunkBlockData[topBottomBlockIndex];
        _MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
        chunkBlockData[topBottomBlockIndex] = localData.data;
    }

    for (int sideIndex = 0; sideIndex < 6; sideIndex++)
    {
        if (1 == random.Next(0, 2))
            continue;
        int sideWidthIndex = widthIndex;
        int sideDepthIndex = depthIndex;
        GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);
        int sideBlockIndex = GetDataArrayIndex(sideWidthIndex, sideDepthIndex, heightIndex);
        localData.data = chunkBlockData[sideBlockIndex];
        _MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
        chunkBlockData[sideBlockIndex] = localData.data;
    }

    if (1.0f > removeHeat)
    {
        if (removeHeat > random.NextDouble())
            blockData.heat = (byte)Math.Max(blockData.heat - 1, 0);
    }
    else
    {
        blockData.heat = (byte)Math.Max(blockData.heat - removeHeat, 0);
    }
    return blockData;
}

private void _MoveHeat(ref BlockDataSt blockData, ref BlockDataSt targetBlockData, ref float removeHeat, ref IHexWorldUtility worldUtility)
{
    if (blockData.heat > targetBlockData.heat)
    {
        BaseBlockLogic targetLogic = worldUtility.GetBaseLogic(targetBlockData.materialId);
        if (null != targetLogic)
        {
            float addHeat = Math.Max(targetLogic.thermalConductivity, 1);
            Random random = worldUtility.GetThreadRandom();
            if (targetLogic.thermalConductivity > random.NextDouble())
            {
                targetBlockData.heat = (byte)Math.Min(targetBlockData.heat + addHeat, byte.MaxValue);
                float ratio = specificHeat / targetLogic.specificHeat;
                removeHeat += addHeat / ratio;
            }
        }
    }
}

リアルタイムの熱伝導の様子をビジュアライズする機能を追加してデバッグすると次の通り
www.youtube.com

デバッグ目的で大気の温度を最大にしてチェックしていますが、地上と宇宙に接している部分から熱が逃げていき、徐々に大気が冷えている様子がうかがえます。

・温まった空気ブロックが水分を水ブロックから吸収

まずは水ブロックも世界に登場させないとですね。
登場させました。(色々仕様変更していたから、導入に手間取った…)

f:id:simplestar_tech:20180219001525j:plain

大気は温度によって水を含める量が変わることにします。
例えば 0 ~ 255 の温度の中で、温度0の空気は水を含めないとし
温度が上昇するにつれて、徐々に水を含めるようになっていき
温度255で最大の255まで水を含めることとします。
(本当は比例しないのですが、処理を簡略化するために比例することにします。)

基本的に水に接している空気は、水ブロックから限界まで水を吸収します。
水ブロックは最大の255まで到達した空気ブロック10個と等価で、合計2550を 255 で割った 10
つまり、10 ずつ空気に分配し、その時に水ブロックから 1引きます。(または、ここにも確率の要素をいれて、空気に+1しても 1/10 の確率で水ブロックで -1 とか)
255から始まって、0になった水ブロックは最後、空気ブロックになります。
これが蒸発の再現です。

温度ごとに含めることができる水の量は決まっていますので
高温多湿の空気が冷えることで、余った水が雲粒として空気中に現れます。(ブロックとは異なるエフェクトで可視化)
余った水の量に応じて雲の密度を段階的に上げます。(エフェクトを濃くするとか)

余った水の量は基本的に下の空気ブロックに渡し、渡しきれないときは繰り越されます。
これが行われる時、雨が降る状態となります。
空気中に255以上の水が含まれることが地上付近で起きると、この空気ブロックは分量1の水ブロックになります。
これが降水の再現です。

ロジックを書いてみます。

・暖気の上に冷気がある場合は上下ブロックの内容を交換

ボイルシャルルの法則によれば、等圧下における高温は低密度だそうで
単位体積あたりの質量は暖かい方が小さく、軽いということになります。

ブロックのマテリアルに新たに比重と密度を与えます。
ブロックの体積はすべて一定です。

土の比重に比べて、水の比重はさらに大きく、金の比重はさらに大きいとします。
密度は土よりも水、水よりも金の方が大きいなど

基本的に水と空気などでは、密度 x 比重によるブロックの質量を算出して
高い質量のブロックが重力方向に向かって交換が行われます。(落下を表現)

同じ比重、密度のブロック同士の場合は、温度と圧力の関係で圧力を温度で割った値を気体のエネルギーとします。

同じ比重と密度の気体同士が重力方向で重なっている場合、比較して温度が高い気体はエネルギーが小さいことになり
上の気体ブロックと交換します。(上昇を表現)

ただし、上のブロックの圧力が低く、同じエネルギーまたは、低温でも上の気体のエネルギーが小さい場合は、交換は発生しません。
ただし、気体は周囲の気体の圧力と同じ、または一つ上の気体より1大きい圧力になります。
すると、もしその状態で下のブロックのエネルギーが小さい場合は、上昇します。

これにより、一般的に上空は低温で低圧、地表は高温で高圧、しかし、太陽の輻射熱で地表があたたまり、熱伝導により空気が温まり
その空気のエネルギーが小さくなった場合は、暖かい空気が上昇します。

おまけ

立体的な数値の時間変化をデバッグするため、見た目で結果がわかるようにしなければなりません。(ログから想像する?別に頑張ってもらってもいいよ)
そこで、仕事帰りの時間でしばらく調査を続けていたのですが、Unity の Particle System を使って固定パーティクルでブロックの状態を表示する方法を思いつきました。

参考記事はこちら
qiita.com

パーティクルごとにサイズや色を設定できるため、なかなかに便利です。(パフォーマンス面は不安ですが)
f:id:simplestar_tech:20180208234241j:plain