FreeCADからcfMesh作成用の設定ファイル一式を自動生成するスクリプトとケースファイルを紹介した記事のなかでお約束したスクリプトの解説を以下に記しておきます。
但し先の記事の中でも記したように、筆者は本職のプログラマでないので、ええ加減、あるいは間違っている部分もあるかもしれないことを最初にお断りしておきます。また、こうして解説という形でプログラムを見直してみると、もっとああしたら、こうしたら・・・という箇所がたくさんあるし、何よりも下手くそな事やってるなぁ!など、とても他人様に見せられたものでないなぁとお恥ずかしい限りです。
それでも敢えて公開するのは、備忘録という面もありますが、どなたかにこのプログラムを引き継いで開発してもらいたいという期待があるからです。また、これが出来てこそのオープンCAEだからです・・・ということで宜しくお願いします。
全体の流れ
クラスや関数定義(def文)の中味をトップレベルで折り畳んでプログラム全体を総覧すると、以下のようになります。
なおこの閲覧表示には、SPE(Stani’s Python Editor)というpythonの統合開発環境(DEXCS for OpenFOAM(R)には標準搭載しています)を使ってています。
これは、キーワードを色分け表示したり、階層部分を折り畳んで表示するなどして、全体の総覧が判り易くなる為です。
最初に、import文で、必要なライブラリモジュールが組み込まれ、その後、class文やdef文で定義される様々なクラスや関数が定義してあって、一番下にメインプログラムがあります。
pythonプログラムというのは大体このような構成(フレームワーク)になっているようですが、最初からスクラッチで作ったものではありません。本例の場合には、wx-pythonのサンプルプログラム(確かTableGridを取り扱ったものの一つ)を持ってきて、その段階で本例でキーとなるTableGridを取り扱う為のフレームワークは出来ているので、関数の名前や、中味を書き換えていくという方法で作ったものです。
メインプログラムの中味は、
と、いたってシンプル。ここで注目していただきたいのは、L.447の
frame = objSetting(None, sys.stdout)
です。objSettingという名前(メインプログラムの中で、参照したwx-pythonサンプルプログラムから変更したのはこの部分だけです)になっているので、これでL.120の関数定義が実行されるわけですが、このframe を定義して、その次の行で、これをShow、さらにその下でMainLoopってことは、この処理をずっとループさせてるってこと。何かイベントが発生したら、定義されたプログラム(何が定義されているのかは関数の詳細を調べないと判りません)が起動するという仕組みです。
ここでカッコ()の中の引数が、Noneとかsys.stdoutとなっており、これらは引き合いにしたwx-pythonサンプルプログラムにあったものをそのまま使っています。引数そのものの意味はある程度推定できるのですが、これらを引数にしなければならない理由とか、これを変えたらどうなるか(無闇に追加したりすると叱られます)・・・など良く判らないまま作っています。
プログラムの本体は objSetting
ということで、この中味を調べてみます。
またしても、def文がたくさん出てきました。
最初に__init__()と特殊な名前のdef文(クラスの中ではメソッドと呼ぶんだそうです)があり、これはクラスのインスタンスが生成された直後に呼び出される決まりになっているそうで、この構成までは引き合いにしたwx-pythonサンプルプログラムにあったものをそのまま使っていて、その中味を今回の用途に合わせて書き換えたのと、それ以下のメソッドは今回新たに追加したものになります。
それでは、次に__init__()の中味を見ていきます。
基本的には、GUI画面(パネル)の設計図のようなもので、L.140〜L.151あたりで、このパネルに配置している部品のクラスや名前を定義しており、これらをどうレイアウトしているかを決めているのがL.159〜L.173です。これら部品のうち、このアプリでキーとなる部品が表形式のテーブルで、これだけは
self.grid = CustTableGrid(p, log)
として、CustTableGridという別のクラスを呼び出して定義しています(後述)。
なお、この部分、この部品だけにself.がついていたり、引数の中味が(p,log)と記されており、この前段でpやlogが定義されている部分については、件のサンプルプログラムで使っているものをそのまま転用しているだけで、そうすることの意味をちゃんと理解して使っているわけではありません。
かなりゴチャゴチャしていますが、GUI画面と対比させながら読むと理解しやすいと思います。
あと独自の追加項目としてL.123の、
global caseFile, maxCellSize, minCellSize, featureAngle
これら4つのパラメタ(caseFile, maxCellSize, minCellSize, featureAngle)の内容(具体的数値)は、GUIを通してユーザーが任意に設定変更できるもので、他の関数やメソッドに渡して使用するものです。
当初はglobal変数にするつもりはなかったのですが、引き渡す際の方法(引数で渡すのか、self.なんたらで書けば良いのかなど)がわからなくて、やむ無くglobal変数にしたといういきさつがあります。今になって思えば、self.なんたらという書き方をしておけば良かっただけの事かもしれません。
また、L.126
cellMax = searchBound2cellMax()
では、FreeCAD上で表示されているすべてのobjのBoundBox情報から、領域全体のBoundBox情報をベースに、概略のセルサイズ情報を算出しています。これをデフォルト値として、maxCellSizeのテキストボックスに表示させています(後述)。
TableGridの作成
L.140で、
self.grid = CustTableGrid(p, log)
として、クラスCustTableGridを呼び出す、そのクラス定義はL.101にあって、
ここからさらにL.104
self.table = CustomDataTable(log)
でクラスCustomDataTable(L.24)を呼び出す、という構造になっています。
ここも当然__init__()というメソッドがあって、それ以外にもたくさんのメソッドが定義されていますが、__init__()以外のすべてのメソッドは、件のサンプルプログラムをそのまま使っています。
このクラスを利用することで、__init__()の中味で、このテーブルに対するプロパティと初期値を定義しておけば、その他に定義済みのメソッドを使ってテーブルの内容を変更できるようにしたり、変更した時にその内容を取得したり、といったことが出来るようになっている訳です。
その他のメソッド(たとえばGetColLabelValue)の役割は、ほとんどその名前(と英文のコメント)から想像できると思いますが、その具体的なプログラム内容は知らなくても)使えるようになっているところがポイントで、ここでもその中味の説明は致しません(というか出来ません)。
それでは、__init__()の中味を見ていきます。
L.28でテーブル1行目の見出しセルの内容を定義しています。
L.29〜L.36では、各列のデータ形式を定義しています。
そして、L.42〜L.44を使って、テーブルに初期値を与えているわけです。この部分だけ若干説明しておくと、docというのがグローバル変数(L.19,20参照)で、FreeCADで開いているモデルそのものになります。これのobjすなわちdoc.Objects(L.42)はモデルに含まれるコンポーネントを意味して、obj.ViewObject.Visibility(L.43)はモデルコンポーネントの中の表示されているもの、という事になります。
したがって、表示されているモデルコンポーネントについて、obl.Label(その名前)を1列目に、2列目には、とりあえず”patch”を選択、、、、(L.44)ということをやってるわけです。
以上で、クラスobjSettingの__init__()というメソッドで、このスクリプトを起動した際にデフォルトのGUI画面が作成されるまでの仕組みを見てきたことになります。
次に、クラスobjSettingのL.154〜L.156では、3つのボタン(b1,b2,b3)のそれぞれで、ボタンが押された時(wx.EVT_BUTTON)の実行関数(メソッド)を定義していますが、それぞれの内容について調べていきます。順不同で、簡単なものから説明します。
Exitボタン
155行目で
self.Bind(wx.EVT_BUTTON, self.OnExitButton, b2)
OnExitButtonが指定されており。この内容は、L.403で、
内容は説明するまでありません。ただ本当はClose()する前に、変更があったらその内容を保存しておくとかの機能も追加したいところですが、そこまで手が回っておりません。
caseボタン
L.156で
self.Bind(wx.EVT_BUTTON, self.selectCase, b3)
selectCaseが指定されており。この具体的内容は、L.178で、
となっており、wx.DirDialogという、いわゆるファイルマネージャーでフォルダーを選択する際に定番で出てくる画面を使っています。ここで選択したフォルダー名が、caseFile.Label にフルパス名で書き換えられる訳です。
ここ(caseFile.Label)には、デフォルトでは、FreeCADのモデルが存在していたディレクトリ名が入っていた(L.144←L.132)のに対し、メッシュを作成したいフォルダ名が異なる場合に使うことを想定しています。
なお、本当は、ここで選択した後に、そのフォルダがOpenFOAMのケースファイルになっているかどうかをチェックして、そうでない場合に警告を出すなどしたかったのですが、そこまで手が回っておりません。
Exportボタン
L.154で
self.Bind(wx.EVT_BUTTON, self.exportSettingFiles, b1)
exportSettingFilesが指定されており。この内容は、L.191以降で、
となっており、ここからが一番肝心な部分です。
L.192〜L.195で、GUI画面の中で、テーブル以外の部分でユーザーが書き換えた(かもしれない)情報を取得しています。
L.198で作成するかどうかの確認ダイヤログを表示、OKとなった場合、以下4つのステップで設定ファイルを作成します。
- STLファイルの作成
- fmsファイルへの変換
- fmsファイルの修正
- system/meshDictの作成
以下、順番に説明します。
STLファイルの作成(L,224:def makeStlFile)
FreeCADのオブジェクト毎にSTLファイルへ出力しているのは、L.233〜L.249の部分ですが、これらのほとんどの部分は、オープンCAE勉強会@関西の有志メンバーにて作成されたものをそのまま借用しています。
ここでは、オブジェクトの中で、grid.tableにリストアップされており(L.234)、Typeがregionのもの(L.235)を出力するように手を加えました。
fmsファイルへの変換(L.207〜L215)
stlファイルは、単に面情報しか持ちませんが、surfaceFeatureEdgesコマンドによって、稜線情報を含んだfmsファイル形式に変換しています。(詳細の説明はこちら)
4つのステップのうち、なんでここだけがdef関数化されてないの?と指摘されそうですが・・・
fmsファイルの修正(L217⇒L.259:def modyfyFMS)
fmsファイルは面の幾何学的情報の他に稜線情報も含んでいるだけでなく、形式的には面のtype情報も定義できますが、単にsurfaceFeatureEdges変換しただけでは、面のtype情報を組み込むことは出来ません。通常は、type empty として取り扱われるようです。
そこで、ここでは<empty>が、テーブル第2列目(Type)で指定した境界typeになるよう、sedコマンドを使って書き換えています。但し、単純サーチしただけでは境界ごと個別に書き換えができないので、テーブル中でのregion以外(stl出力されるオブジェクト)の並び順に応じて、書き換え範囲を限定してsedコマンドを適用するという方法で対処することとしました。
あまりスマートなやり方でないなぁ・・・と思いつつ、色々方法を試しても上手くいかない状況が続いたなかで、まぁこの方法ならとりあえず動いてくれるようになったし、今のところ不具合例もなさそうなので・・・というレベルです。もうちょっとスマートなやり方をしたいところです。
その後、cfMeshマニュアルを読みなおしたところ、後段のmeshDictを作成する際に、renameBoundaryというセクションの中で定義してやれば良さそうという感触を得ましたが、今後の課題です。
system/meshDictの作成(L219⇒L.280:def makeMeshDict)
L.300以降省略しており、関数ブロックとしては、この部分が一番長いのですが、cfMesh作成時に必要となるsystem/meshDictをオープンし(L.282)、以下、一行ずつベタに書き込んでいるだけだからです。
GUI画面にユーザーが書き込んだ数値は、L.294のようにして取得できるので、他の数値も同様にしてやっています。
唯一補足しておきたいのは、以下のobject refinement の定義ブロックを作成する部分です。
ソース中のコメント文にも記してあるように、 Box要素であることを前提に作ってあります。cfMesh本来の仕様では、
The supported objects which can
be used for refinement are: lines, spheres, boxes, and truncated cones.
となっているので、Box要素以外にも対応させたいところですが、そこまで手が回っておりません。
その他
searchBound2cellMax(L.412)
ここでは、FreeCADで開いているモデルの全オブジェクのなかで、表示されているオブジェクトについて、寸法の最大・最小値を調べています。
FORTRANプログラムのような書き方をしていますが、素の地が出たということでご容赦。また、最大値の初期値として、-10000としています(L.413)が、もちろんこれじゃ万能でありません。意地悪なモデルには対応できていないということです。
初めてFreeCADのpythonを経験する人にとって、obj.Shape.BoundBox.XMaxといったプロパティの名前をどうやって知ったら良いのか?という問題があると思います。そのあたり詳細なマニュアルがある訳ではなく、Pythonコンソールの使い方マニュアルを理解して自分で調べろ!ということらしいです。調査例のイメージを下に掲載します。
round_sig(L.440)
上のsearchBound2cellMaxでの計算は倍精度計算で、結果は長大な数字の列になってしまい、これをそのまま表示したのでは見苦しくなってしまいます。有効数字を丸める方法はないかと調べていたら、彼山の知識というサイトで見つけて、そのまま借用しました。
L.441はなんとなく理解はできますが、これを自分でゼロから作れと云われるとまず無理でしょうな・・・このあたり素人プログラマの限界です。
追加したい機能
本文中の所々で、手付かずで未熟な部分についてコメントしておきましたが、ここではまだ触れていない項目について記しておきます。
設定保存機能
起動したばかりのデフォルト状態から、様々な設定を施しますが、一度終了してしまうと最初からやり直しで、その手間も馬鹿になりません。当初はそういう設定状態を保存するのに、専用に設定ファイルを作るアイデアで構想していましたが、それよりも起動時、またはcaseボタンを押してケースフォルダーを変更した際に、そこに存在するfmsファイルや、system/meshDictの中味を読み取って、GUI画面に反映(するかしないかはユーザーに決めさせる)方法の方が良さそうだと思っており構想中です。
その他にもいろいろ
あったと思いますが、まとめきれていません。また、ご要望・ご意見などあれば、適宜ここにリストアップしていく予定です。
最後に
GUI画面のレイアウト
は手作業で実施している為、読み難くなってしまいました。
当初、DEXCSランチャーやTreeFoamと同じく、wx-gladeを使って作成するつもりでしたが、画面レイアウトがなかなかしっくり来なかったのと、wx-gladeで使うテーブルの取り扱い方がよく判らなかったこともあって、wx-tutorialを雛形に手作業で修正することになったという舞台裏です。
ただ、じゃぁ今のレイアウトで満足しているかというと、大いに不満です。かろうじて及第点のレベルで、直したいところは山ほどあるんですが・・・
また、
FreeCADから起動するpythonスクリプト
であるということから、プログラム開発上、なかなか厄介な面があります。早い話がスクリプトを単体で実行できないということです。実行しようとすれば、ソーズ冒頭(L.4)の、
import Mesh
で、Meshがないよ!といっていきなり叱られてしまう訳です。
仮にこれを解消できたとしても、FreeCADで対象としているモデル情報(ファイルの名前)をどうやって引き渡すかの方法もわからないという問題もあります。
また、そもそもスクリプト単体で実行したくなるのは、FreeCADのマクロ画面からの実行だと、実行時のエラーメッセージが表示されない場合が頻出するからです。途中まで正常に実行できていて、どこでエラーが生じたのかが判らない。デバッグ用にprint文で出力させても、そのメッセージが何処に掃き出されるのかもわからない。
「Export」ボタンを押した後、何度も異なったメッセージダイヤログが出力されるのは、ユーザーさんにとってはどうでも良いことでウザイと思われるかもしれませんが、うまくいかなかった場合に、上述のように実行時のエラーメッセージが出ないので、どこに問題があるかを特定するのに重要な足掛かりになるからです。
このあたり、FreeCADのpythonに詳しい方なら、解決策をお持ちのような気もしますがいかがでしょう?