はじめに
現在研究用にシミュレータを自作しており、その過程で学んだことや思ったことをつらつらと挙げていきます。
何かご指摘があればコメントください。
知識
使用するプログラミング言語に慣れていることを前提とします。その上で、プログラミング言語に係らず必要となる概念・技法等の知識がたくさんありますので、色々な本やオープンソースプロジェクトのソースコードを読んで勉強することをおすすめします。「技術書ランキングサイトをQiita記事を集計して作ったら、2700冊の技術本がいい感じに並んだ」のランキングから良さそうなものを拾い読みしてみてもいいでしょう。
個人的に良書と思ったものは下記の通り:
- リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック
- オブジェクト指向でなぜつくるのか 第2版
- C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス
- C++テンプレートテクニック 第2版
- レガシーコード改善ガイド
今まで利用したり読んだりして勉強したオープンソースは下記の通り:
ただし数値計算分野のオープンソースのコードの構造や書き方が必ずしも良いとは限りませんので、信用し過ぎるのはやめましょう。
(実際OpenFOAMは継承を多用しすぎていて使いづらいと思うことが多々ありました。)
心得
学生・研究者のほとんどは職業プログラマーではないため、とかく「プログラムは動けばいい」と思いがちです。ですが研究は継続されるものです。その研究を引き継ぐ人がいることを忘れないようにしましょう。そのために以下のことは最低限守ってコードを書くようにします。
- git等のバージョン管理システムを使う(GitHubやBitbucket等と連携させることも忘れずに)
- 正確性・簡潔・明快さを再優先とする。
2-1. KISS(Keep It Simple, Stupid / Keep It Short and Simple)の原則
2-2. なるべく一つのクラス・関数に一つの仕事のみを担わせる
2-3 グローバルデータ・共有データを避ける
2-4. 長い関数・深いネストを避ける
2-5. わかりやすい変数名をつける(コメントを付ける必要がないくらいに) - 標準ライブラリ・外部ライブラリを積極的に利用する(vcpkg, conda, conan等のパッケージマネージャを利用する)
- doxygen等を使ってプログラムのドキュメントを作成する
- ビルドを自動化する(c++なら最も広く使われているCMakeを使用する)
- テストを書く(googletest/catch2などを使用する)
慣れないうちは中々できませんが、少しずつできるように努力していくことが大切です。
作り方
シミュレータの仕様を決める
会社でソフトウェアを開発する場合は、仕様書、設計書、詳細設計書などを作成して進めていくのでしょうが、研究用に自作する場合はそこまでする必要がありません。まず研究目的に合わせて、シミュレータの仕様をざっくりと決めます。この段階では
- 入力データ
- 出力データ
- 数値計算手法
を決めておけばいいと思います。最初の段階ではあれこれ機能を加えず、必要最小限に機能を絞って仕様を決めてください(YAGNI原則)。あれもこれも欲しいと考えて大風呂敷を広げると、大抵の場合は碌な事になりません。
シミュレータに必要なクラス・関数を書き出す
次にシミュレータ内で使われるクラス・関数を書き出していきます。どんな機能を持ったクラス・関数が必要なのか、それぞれの入出力は何かをざっくり書き出します。注意して欲しいのは、この段階ではまだクラス・関数の実装は行わないということです。すなわち、クラス・関数が具体的に内部でどういう処理をしているかまでは具体的に考えず、それらの入出力(インターフェイス)のみを設計するということです。
各クラス・関数を実装する
各クラス・関数を実装します。この段階で実際にコードを書き始めるのですが、各クラス・関数ごとに次のサイクルを回していきます。
- クラスまたは関数を実装する。
- 単体テストのためのコードを書く。
- 単体テストを行い正しく動くことを確認する。
単体テストを書きづらいと感じたら、そのクラス・関数の設計がどこかおかしいということです。単体テストを行いやすいように設計を変更(リファクタリング)しましょう。
シミュレータを作成する
必要なコンポーネントが揃ったらシミュレータを作成します。とりあえずはmain関数内にがんがん書き込んで行きましょう。書いている内に欲しいクラス・関数が出てきたら、
- 必要なクラス・関数を作成する。
- 作ったクラス・関数を使ってmain関数を書く。
ということを繰り返します。
シミュレータのテストを行う
シミュレータが動いたら解析解との比較を行い、正しく計算できているか確認します。
リファクタリングする
とりあえずシミュレータができたわけですが、大抵の場合必ず読みにくい・使いづらい部分があるはずです。大きなクラス・長い関数をリストアップして細分化したり、入出力を変更したり等を行います。KISS原則を念頭に置いてリファクタリングします。
簡潔な設計と明快なコードの価値は、どれだけ強調してもし過ぎることはない。
(中略)
6ヶ月前の自分が一体何を考えていたのか思い出そうとするとき、明快なコードを書いた自分自身に感謝するというのもよくある話だ。Herb Sutter/Andrei Alexandrescu, 「C++ Coding Standards ー101のルール、ガイドライン、ベストプラクティス」
これ↑よく実感します。
機能を追加する
機能を追加する場合、大抵の場合は元の構造を多少壊す必要があります。したがって、機能の追加はリファクタリングと必ずセットで行います。
シミュレータ作成の実例(私の場合)
基本的に上の流れに沿ってシミュレータを作成しました。ただし、「各クラス・関数を実装する」段階においても、少しでも使いづらいと感じたら即座にリファクタリングしていました。
今回作成したのは、岩石内部のフラクチャーの変形・伸展とフラクチャー内部の流体流動を計算する水圧破砕用シミュレータです。等方線形弾性体の変形を表すNavier方程式と流体の流動方程式を解く必要があり、最初に下記のようなシミュレータ仕様を決めました。
- 分離型ソルバー(Navier方程式と流動方程式を交互に解く)
- Navier方程式はDDM(Displacement Discontinuity Method, 境界要素法の一種)を使って解く。
- 流動方程式はFVM(Finite Volume Method, 有限体積法)を使って解く。
- メッシュ
- 行列計算
- Eigenライブラリを使用する。
- 計算結果の出力
- VTK Legacy形式にメッシュデータとまとめて出力する。
- 単体テスト
- Google Testを使用する。
- メッシュ以外のシミュレータへの入力データ(初期条件・各種設定など)
- JSON形式で作成する。
- JSON形式の読み取りにはJON for Modern C++ライブラリを利用する。
最終的に出来たライブラリの構造はこんな感じです。
/-----app/ # 各種シミュレータの実装
|---test/ # 単体テスト
|---doc/ # Doxygenを使ったライブラリの説明用HTML作成場所
|---src/
|---ddm/ # DDMに必要なクラス群
|---field/ # 流動・DDM計算に必要なフィールドをまとめて作成・保持するクラス群
|---equation/ # 方程式の残差・ヤコビアン行列を計算するクラス群
|---solver/ # Eigenライブラリの密行列・粗行列ソルバーを呼び出すクラス群
|---model/ # 流体・フラクチャーのプロパティを計算するクラス群
|---mesh/ # メッシュの読み書き・操作・独自メッシュデータを作成するクラス群
|---utility/ # シミュレーションの時間・反復を操作するクラス群