数値計算もソフトウェアシステムとして開発されていい

タイトルのとおりですが、自分がイカした数値計算システムをつくって公開したとかそういったことは全然なく、むしろ逆でまともなアーキテクチャのシステムをつくることができなかったために発生した苦労との闘いと「ああすればまだマシかもしれない」をしたためた感想文を書くことにしました。ツールを使った場合は感想文を書けるほどの経験がないので、フルスクラッチしていて保守性の悪いコードを書いてしまったときの体験の悪さでも残しておこうと思います(「こうすればよかった」的な振り返りも考えましたが未だ完全な実装に至っていない部分もあり、そういうのは「ぼくのかんがえたさいきょうのシステム」程度の戯言なので、これを見た方は無視しましょう。数値計算プログラムを書き始めたときにソフトウェアエンジニアリングに関する知識がゼロだったので、色々失敗も書きますが優しく見守ってください m( _ _)m)

数値計算を行う手順

システムとして開発されていいなどと言う前に、私が書いていた数値計算プログラムはどんなつくりをしているのかというところからまとめます(ちなみに私が経験があるのは流体の数値計算です)。数値計算といっても様々な種類があるわけですが、力学の問題を数値的に解くいわゆる計算力学では、何らかの支配方程式を解くためのソルバがあるというのが一般的かと思います。例えば、研究室などでははじめに「自分で書いてみてよ」とか言われると思いますが、さぁ論文や教科書を片手にコードを書くゾイとなる前に、一度ここで数値計算をするプログラムがどんな作りになっているのかを把握したほうが後々幸せになれます。文献片手にプログラムを書けはしますが、振り返ってみると、これをやって良いのは「一次元で格子数が決まっている場合」「再現実装等で書き捨ててよい場合」くらいな気がするんですよね。というのも、汎用性などを考えると数値計算プログラムというのはデカイ金槌1個ですべてを蹂躙しようというモチベーションでは片付けにくい面倒な手続きがあるからです。下が数値計算を行うプログラムの構造と処理の大体の流れかなと思います。

支配方程式をソルバに解かせる前に、解きたい問題の初期条件は何か、境界条件はどうなっているのか、空間・あるいは物体の形状はどのようなものであるのかを指定する必要があります。1次元なら物理量を保存する配列1個でガシャガシャやっても初期条件や境界条件の扱いにも困ることはないでしょうし、結果の出力や後処理もソルバプログラムに追記する形で扱って特に問題はないはず。ただし、2次元以上になると、(x,y)や(x,y,z)で格子数を決めて、面や立体として形状を定めなければならず、初期条件や境界条件にもバリエーションが増えます。形状も解く問題によって様々なものが登場し始め、1次元のソルバでは問題にすらならなかったメッシングも手間暇がかかるようになるので、外部に切り離したくなります。さらに、2次元以上になると現象の概要を掴むためにParaviewなどで可視化したい、とか、特定の部分だけで分布をみたい、とか結果の分析に関するユースケースも爆増します。そのために、多様な後処理用のプログラムを外部にもっておくほうが分かりやすくなるので、後処理部分は切り離します。これを一連の流れに数値計算プログラムは成り立ち、結果として使う情報というのは上の図で一番右に出てくる後処理後のファイルなので結果を得るのはまぁまぁな苦労です。

プログラムや成果を管理する上での苦悩

さて、こういう構造になってしまう数値計算のプログラムは当然複雑になるし、入出力をはじめとして細々とした面倒事に関しては枚挙に暇がありません。ここでは主に面倒事として感じていたことをまとめていきます。

ソルバ実装上の面倒なところ

  • 条件のバリエーション
    まずは初期条件や境界条件、メッシュのバリエーション、そして「〇〇な問題を解くには△△なソルバがいいよ」と言った具合にソルバの選択にもバリエーションが生じる場合もあると思います。多数の選択肢がある条件を、内部の制御をポチポチやりながら切り替えていくのはかなりダルいですが、資料片手にノリで書き始めたプログラムを改修していくようなやり方を取るとこれをやってしまいがちなんですね。(私も友人氏も後輩氏も先輩氏も条件に応じて多数の処理をベタ書きし、コメントをつけたり外したりするやつをやっておりました)。しかし、「さすがに怠すぎる」と思う頃にはもうプログラムの骨組みはでき上がっており、イチから構造を見直すとなると「いや結果速く出さないと...」みたいになり、抜本的な作り直しもできず...といった負のスパイラルは早くもここで完成します。私は悪あがきでコマンドライン引数に計算条件の名称を入れ、それを元に大枠の条件設定が分岐するように変更しました。しかし、カッチリした条件だけで計算するはずもなく同じ形状のメッシュでも格子数が違う、とか、少しだけ初期条件に変える、とか細かい変更があるので、コメントつけたり外したりを完全に消去することはできませんでした...。

  • 速さが足りないッ!!
    これは別に速い言語がどうこうを主張したいわけではなく、処理に使うデータ構造で工夫できる点があるならやったほうがいいということです。愚直にロジックを書くよりも性能が出るデータ構造やアルゴリズムがきっとあるはずと思うことが第一歩。賢くないロジックを書けばどんな言語でも遅くなると思いますし、言語は設計の悪さを改善してくれはしないので、ここでは特に言語に関する言及はしません。

  • コーディングスタンダードをよく考えたい
    これは私だけかもしれないですが、物理量に関してはどんな切り方をしても全てののサブルーチンで参照するといっても過言ではないくらいなので、グローバルな扱いの方が逆にいいのではと思うことは結構ありました。私は知識のない状態からコードを書いていたので、何が起きるかわからないことへの恐れから意地でローカル変数にしましたが、プログラムの全体が把握できるようになるとグローバル変数で扱っても悪くない気がします(小話ですが、そこまで条件のバリエーションが多くない計算をやっていた友人氏は「分けてもどうせどのルーチンでも扱うし、1個のファイルで見られる方が良くない?」と1つのプログラムファイルに全ての処理を書くという方法をとっていました。パワープレイすぎる)。グローバル変数は一般には忌み嫌われる存在ですが有用と思われる部分もあるように、数値計算をする上でのコーディングスタンダードを考えたいと思っていました。

    • 言語仕様で多少の違いはあるでしょうが、例えばFortranならmodule, C系だとstructとかをグローバルに準ずる存在にしておくのもよかったかもしれないです
  • テストがダルい
    マジでひたすらにダルい(嗚咽)、うまく動かないときはかなりの確率で難解なデバッグが実装者の身に降りかかります。そもそも設定条件に様々なものがある上、時間を刻んで計算を何ステップも繰り返して進行させるため、どの部分が一番怪しいのかは経験則に頼る部分が大きくなります(経験が浅いときにソルバの実装がうまくいかず、初回ステップのソルバの計算過程を全部手計算したときはさすがに虚無過ぎた)。しかし、それもソルバに問題がなければ意味がなく、条件設定やメッシュの方を疑うことになります。私の経験では他にもこんな感じのがありました。見ていればなんとなくわかると思いますが、可視化して確認とか、グラフ書くとか、確認のために必要な作業で最もコストがかかる上に肝心なところが自動化できなさそうなのが厄介ポイントです。

    初期条件の設定ミス
       境界付近が怪しげな値になるのをたまにやる。最初に空間全域の設定値を出力したりしてなんとか特定する。
    境界条件の実装ミス
       2次元以上になると最初にミスりがち。単純なメッシュと単純な初期条件を使って境界条件の計算結果を確認したりする。
    ソルバの実装ミス
       テスト問題を解いて理論解や文献と照らし合わせてグラフ書いて合ってるか確認したりする。
    メッシュが悪い
       後輩氏が発散しまくるので何事かとメッシュを可視化してこまかーくみていくと不必要にギザギザな形をした部分があり、擾乱がここから発生していた 。

計算結果の管理で面倒なところ

  • メタデータが多い
    数値計算の結果は、それを分析するデータを生成する後処理に必要な情報や、分析自体に必要な情報も結構あります。似たような計算をたくさんこなすこともあり、ファイルの管理も大事。計算結果の管理に必要な情報をパッと思いつく限り上げてみると...

    1. 初期条件
    2. 境界条件
    3. 問題の概要(問題には名前がついていたりするので、問題の名称とかで良いとは思う)
    4. 使用したメッシュとその格子数
    5. ソルバの計算結果のファイル名

こんな感じでしょうか。計算条件は言わずもがな、あと使用したメッシュなどが、後処理の入力となる計算結果のファイルと共に紐付けて記録しておかないと、違うファイルを参照して後処理するなどのミスが起きやすくなります。個人的にはディレクトリ構成でなんとか管理するやり方はまずいと思いました。あとから深くなっていくし、計算条件の変更に対して柔軟でなかったです。

  • ファイル同士の関係性を把握する必要がある
    前項と関連する話で、後処理をして分析対象となるファイルには前項に列挙したような情報を紐付けておくのは結果の管理において必要なコストだと思います。そういった関連性を記述する実装がなされていてもいい。具体例を上げるとParaviewで可視化するためにvtkファイルを作りたいみたいな場合ですね。可視化するためのファイルを使うには「どんな形状の空間(物体)で」「どんな物理量の分布をしているか」の情報が必要なので、物理量を吐いたファイルと、使用したメッシュを対応させて後処理しなければなりません。対応しないファイル同士で後処理なんかして再現性のない結果を成果と勘違いしたりするとまずいわけです。

ソルバもソフトウェアエンジニアリングの知見のもとに書くほうがいい(ので、どうすればマシになったか考えるの部)

そんなこんなで、苦労した点をつらつらと書いてみましたが、これを解決するヒントはソルバが一生懸命解いている力学や数学の分野よりは、ソフトウェアとかシステムとかそういった分野の知見にあるというのが本記事の趣旨です。こんなデカいシステムをつくるなら、(当時聞いても概念だけではいまいちよくわからなかったとは思うので意味があるかはさておき)KISS、DRY、YAGNI、SLAPといった原則は知られてもいいはず。こういった概念をどう形にするかはまた別の話だし、「数値計算システムを設計するためのデザインパターンが載っている本とかあるのか...?」って感じなので数値計算屋さんは体験向上のためにも一層ソフトウェアに気持ちを入れて設計を考える必要があると思うわけです(というかこの手の知見がある人はもっと共有してほしい)。一般的なアーキテクチャを考えるのは自分より理解のある人がいるでしょうし、私の場合はどんな考え方、つくりに頼ればよかったのかということを振り返ります。

開発について

プログラムを書く
似たような実装になるものの細かい点が異なる条件設定系の話はソフトウェア的にはポリモーフィズムと呼ばれる実装で解決できそうですし。早さの出るデータ構造も情報系の分野にヒントがあるでしょう。数値計算におけるコーディングスタンダードはちょっと具体的には思いつきませんがそれこそプログラミングの原則をブレークダウンして定めるとかでしょうか。

開発体制とか
ソルバの検証フローの策定はしておくべきでした。まず、初期条件の設定チェックくらいはテストが書けると思います。最終的な計算結果も理論解などと誤差なくピッタリ結果が出るテスト問題を選べば「相対誤差を計算して○%以下ならパス」というテストなどがギリ書けるかと思います(書ける、というだけで是非は怪しいです)。正直、グラフや可視化が検証に必要な時点でテストの全自動化は厳しそうな気がするので、テスト問題の初期条件などを設定する機能を内部に用意して、可視化用ファイルやグラフ化用ファイルをコマンド一発で吐いてくれる仕組みをつくっておくまでが妥協点だったかと思います(と思ったので、ここはMakefileを使ってやりました)。
また、「ソルバの検証だけしたいのに条件設定のルーチンと依存関係にある。問題を解いて検証するにはソルバだけ切り離した実装は難しいが、今うまく動いてるものに検証中のものを入れたくない」と思うこともあるわけですが、これはもうgit使って検証用branch切ればいいだけです。他にも変更履歴を書いておけるなど、gitを使う恩恵は計り知れないものがあります。GitHubやGitLabなりを使って開発して、かんたんな値チェック程度はCI/CDに組み込んで、新たな機能を実装する場合はbranchを切る、開発フローを整理してツールを導入するだけである程度の問題が解消されそうです(さすがにGitを使った開発は行いましたがCI/CDまではやっていませんでした)。

計算結果の管理について

最近、計算結果を管理するためにソルバは計算結果とともにメタデータを出力にする実装にして、メタデータを元に計算結果のファイルと実行可能な後処理を一覧するビューを生成し、行いたい後処理と後処理したい計算結果を選択すればメタデータを読んで計算結果と紐付けて後処理に渡すフロントエンドがあって、後処理はバックエンドのような形で実装すればよかったのではと思いました(まぁ本筋でないことにここまで気合を入れるのもどうかと思うので、趣味の域になってきますが)。何にせよ、計算結果の適切な管理のためにメタデータを書いたファイルはほしいですし、結果の出力時に一緒に吐いておくと良さそうだと思ったので最近JSONメタデータを吐くように変更しました(この程度なら複雑な構造にならないし)。 あとは、入力側もconfigファイルみたいなものを用意して、それを食わせることで初期条件や使用する境界条件が設定すると言った形の実装が良かったんじゃないかと思います(OpenFOAMはこんな感じだった気がする)。ファイルで入力してファイルで結果を管理する、といった設計にはしたかったですね。内部ロジックのコメントアウトポチポチで処理をわける制御はもうやりとうない...。
(例) 結果を出力するときはなんかこんな感じのファイルも一緒に吐く

{
   "XGridNum" : "100",
   "YGridNum" : "100",
   "MeshFile" : "Mesh1.txt",
   "ResultFile:" : "Result1.txt",
   "InflowVelocity:" : "1.0",
   "Outbound:" : "FreeOutFlow",
}

後処理自体はソルバ回すのに比べれば軽い処理の方が多いだろうから、後処理に関する一連の流れは機能がリッチめな言語で実装するのもいいかもです。少なくともFortranで後処理までモリモリやる必要はないわけです(←やった人)。やりたい後処理とそれにつかう結果のファイルを選んでポチッとしたらシュッと結果が出てくるくらいにすれば、「数値計算用のプログラム」としてこれらを全部書いていたときよりもまともな運用ができるはず。設計の基本は役割や責任の分担。どこで役割を分け、どこを別の要素として切り出すべきか、どうやって連携させるのかを「数値計算用のプログラム」というくくりでなく、「数値計算で現象を分析するシステム」くらいの規模感で考えられると良かったです。
最後に余談ですが、スパコンなどを使うこともあり、計算機をまたいだ結果のやりとりなども発生します。こういうのもサーバを建てるという発想になってシステムを作ろうとするかどうかで体験が変わるよなぁと思います。

感想

あとからソフトウェアに関する知識をつけてツールや考え方を取り入れたりして悪あがきはしてみたものの、結局私も抜本的にシステムレベルのものを再構築するには至りませんでした。冒頭で書いたように、力技でソルバを検証して確認が済み、「早く結果を出さないと」と思いはじめたそのときにはもう負のスパイラルは完成しており、イチからつくるには多大な労力と時間をもっていかれます(こういう話も技術者の間ではポピュラーな話だと思います)。そうは言っても、結局使うのは計算結果なわけで、コストのかかる改修を気持ちを入れてイチからやるだけのインセンティブってそんなになかったりします。「情報系でないから」と言わず、数値計算もプログラムを使って行う以上はソフトウェアシステムを構築するための知見が共有されて、開発フローが整っている状態から開発を始める環境はもはや当然のものとして存在していいレベルだとは思うのですが、捉え方によるところもあるでしょう。この記事では私が苦労したところと、悪あがきの内容、振り返ってどうしたらよいか考えたことをできる限り具体的な内容にしたつもりなので、気持ちが出せる人に届けばいいなと思います。
数値計算はそもそも後処理で分析してからが本番なので、「数値計算用のプログラム」は結果を出すためだけのライブラリとしてシステムのコンポーネントレベルの存在であるほうが望ましいと思うようになりました。そうなると、ライブラリになるような実装をするためのインターフェースの設計があるだろうし、プログラムの書き方はだいぶ変わるんじゃないでしょうか。数値計算もそれ用のソフトウェアシステムとして、もっと俯瞰的な設計をしてもいいじゃない。