後方乱気流

シミュレータとOSSのメモ

SALOMEのShaperモジュール + Python3で攪拌翼をモデリングする

勝手にAdventカレンダー16日目の記事です。

動機

SALOMEは, ほぼすべての機能をSALOME内蔵のPythonインタープリタから実行可能です。
簡易3D-CAD機能を提供するShaperモジュールも, 例外ではありません。
サイズ違いのモデルを何通りか作成したい場合, PythonスクリプトからSALOME/Shaperを実行できれば省力化できると思われます。
今回はPythonからShaperを操作し, STLファイルを生成するまでの流れをまとめました。

やりたいこと

Shaperモジュールで攪拌翼, 形状が簡単なパドル翼を生成します。
パドル翼のスパン径や取付角度を自由に変更できるようPythonスクリプトに落とし込みます。

f:id:junkroom:20201216211832p:plain
Fig. 1 sketch
f:id:junkroom:20201216211841p:plain
Fig. 2 result

環境

DockerでUbuntu環境を構築しています。

  • ベースOS : Windows10 Home Edition 64 bit

  • 仮想環境 : Docker / WSL2

    • (Ubuntu 18.04上にSALOMEと依存パッケージ導入済みコンテナを使用)
  • SALOME : v9.5.0
  • Python : v3.6.5(SALOME同梱), v3.6.9(システムPython)

Pythonスクリプトを書いてみる

作業ディレクトリにwing.pyを作成します。

cd dir/to/working-dir
touch wing.py

Salome実行に必要なライブラリをインポートします。
wing.pyを開いて以下を書き込みます。

import salome
import salome_notebook
from SketchAPI import *
from salome.shaper import model
import GEOM
from salome.geom import geomBuilder

攪拌翼クラス Paddle とShaperモジュールの初期化メソッド群を用意します。

class Paddle:
    def __init__():
        pass

    def set_partset(self):
    """
    パーツセットを生成する。
    """
        self.partSet = model.moduleDocument()

    def create_new_part(self, part_name):
    """
    ShaperモジュールのNew Partと等価
    Args:
      part_name (str): パーツ名
    """
        self.parts[part_name] = model.addPart(self.partSet)
        self.parts[part_name].setName(part_name)
        self.part_docs[part_name] = self.parts[part_name].document()

    def create_new_sketch(self, part_name, i, plane='YOZ'):
    """
    ShaperモジュールのNew Sketchと等価
    Args:
      part_name (str): パーツ名
      i (int): パーツ名の末尾につける数字
      plane (str): スケッチ面
    """
        sketch_name = '{}_{}'.format(part_name, i)
        self.sketch[sketch_name] = model.addSketch(
            self.part_docs[part_name],
            model.defaultPlane(plane))
        self.sketch[sketch_name].setName(sketch_name)

ShaperモジュールはPartSetに各Partを登録し、Part内にSketchを保持する構造になっています。
新規PartSet生成は model.moduleDocument()
Partの登録は model.addPart()
Sketchの生成は model.addSketch() といったメソッドで行えます。
戻り値は、生成したオブジェクトクラスのインスタンスです。
インスタンスが持つメソッドやインスタンス自体を使ってオブジェクトを操作するので、戻り値をPaddleクラスのインスタンス変数に保持しておきましょう。

続いて攪拌翼生成メソッド create_wing() を作ります。

    def create_wing(self, h, t, w, m, angle, n_wing, p_name=None):
    """
    Shaperモジュール内にパドル翼を生成する.

    Args:
      h (float): 攪拌翼の高さ
      t (float): 攪拌翼の厚み
      w (float): 攪拌翼の幅(1枚分). 攪拌槽の半径以下に設定する.
      m (float): 攪拌翼の下端から攪拌槽底部までの距離
      angle (float): 攪拌翼の取付角度
      n_wing (int): パドルの枚数
      p_name (str): パーツ名
    """
        if p_name is None:
            p_name = 'wing'

        s_name = '{}_{}'.format(p_name, 1)
        self.create_new_part(p_name)
        self.create_new_sketch(p_name, i=1, plane='XOZ')
        lines = [0] * 5
        lines[0] = self.sketch[s_name].addLine(-t / 2, m, -t / 2, m + h)
        lines[1] = self.sketch[s_name].addLine(-t / 2, m + h, t / 2, m + h)
        lines[2] = self.sketch[s_name].addLine(t / 2, m + h, t / 2, m)
        lines[3] = self.sketch[s_name].addLine(t / 2, m, -t / 2, m)
        lines[4] = self.sketch[s_name].addLine(-t / 2,
                                               m + h / 2, t / 2, m + h / 2)
        lines[4].setAuxiliary(True)
        point = self.sketch[s_name].addPoint(0, (m + h) / 2)
        point.setAuxiliary(True)
        for i in range(4):
            if i != 3:
                self.sketch[s_name].setCoincident(lines[i].endPoint(),
                                                  lines[i + 1].startPoint())
            else:
                self.sketch[s_name].setCoincident(lines[i].endPoint(),
                                                  lines[0].startPoint())
        self.sketch[s_name].setCoincident(lines[4].startPoint(),
                                          lines[0].result())
        self.sketch[s_name].setCoincident(lines[4].endPoint(),
                                          lines[2].result())
        self.sketch[s_name].setMiddlePoint(lines[0].result(),
                                           lines[4].startPoint())
        self.sketch[s_name].setMiddlePoint(lines[2].result(),
                                           lines[4].endPoint())
        self.sketch[s_name].setCoincident(point.coordinates(),
                                          lines[4].result())
        self.sketch[s_name].setMiddlePoint(lines[4].result(),
                                           point.coordinates())
        self.sketch[s_name].setParallel(lines[1].result(), lines[3].result())
        self.sketch[s_name].setParallel(lines[0].result(), lines[2].result())
        self.sketch[s_name].setPerpendicular(lines[0].result(),
                                             lines[1].result())
        proj_z = self.sketch[s_name].addProjection(
            model.selection('EDGE', 'PartSet/OZ'),
            False)
        oz_axis = proj_z.createdFeature()
        self.sketch[s_name].setCoincident(point.coordinates(),
                                          oz_axis.result())
        self.sketch[s_name].setLength(lines[0], h)
        self.sketch[s_name].setLength(lines[1], t)
        proj_x = self.sketch[s_name].addProjection(
            model.selection('EDGE', 'PartSet/OX'),
            False)
        ox_axis = proj_x.createdFeature()
        self.sketch[s_name].setDistance(
            point.coordinates(), ox_axis.result(), (m + h) / 2)
        self.sketch[s_name].setAngle(
            lines[0].result(), ox_axis.result(), angle)
        model.do()
        extrusion = model.addExtrusion(self.part_docs[p_name],
                                       [model.selection('COMPOUND', s_name)],
                                       model.selection(), w, 0)
        extrusion.setName('wing')
        extrusion.result().setName('wing')
        angular_copy = model.addMultiRotation(self.part_docs[p_name],
                                              [model.selection(
                                                  'SOLID', 'wing')],
                                              model.selection(
                                                  'EDGE', 'PartSet/OZ'),
                                              n_wing,
                                              keepSubResults=True)
        angular_copy.setName('wings')
        angular_copy.result().setName('wings')
        lines[0] = self.sketch[s_name].addLine(-t / 2, m, -t / 2, m + h)

Sketchに線を追加するメソッドがsketch.addLine()です。 引数は、(始点のx座標, 始点のz座標, 終点のx座標, 終点のz座標)です(スケッチ面がXOZ面の場合)。 戻り値は変数やリスト、辞書の値として受けとれます。

        lines[4].setAuxiliary(True)

lines[4]は、wing_angleで翼を傾けるときに回転中心を拘束するための補助線です。 setAuxiliary(True)で補助線にしておきます。

        for i in range(4):
            if i != 3:
                self.sketch[s_name].setCoincident(lines[i].endPoint(),
                                                  lines[i + 1].startPoint())

addLine()で線を追加した直後は、線同士がつながっていません。
setCoincident()メソッドで線の末端同士を拘束します。
拘束したい線分は一筆書きで書いておくとfor文で一気に処理できます。

ここでlinesがいくつかのattributeを持つことが分かります。
startPoint()が始点、endPoint()が終点、result()が線そのものを指します。
ここでは線分しか扱いませんが、ArcやCircleを操作する場合はresults()[1]で円弧の線を取り出せます。

後は線分の角度や長さを拘束し、自由度を下げていきます。
setMiddlePoint() : 線分の中点に別の点を拘束 setParallel() : 2つの線分を平行に拘束 setPerpendicular() : 2つの線分を垂直に拘束 setAngle() : 2つの線分間の角度を拘束 setLength() : 線分の長さを拘束 setDistance() : 点と線分までの長さを拘束

攪拌翼をSTLファイルに出力させる関数 export_paddle() を実装します。

    def export_paddle(
            self,
            wing_h, wing_t, wing_w, wing_m, wing_angle, n_wing,
            file_path=None, suffix=0):
        """
        パドル翼を生成する.

        Args:
            wing_h (float): 攪拌翼の高さ
            wing_t (float): 攪拌翼の厚み
            wing_w (float): 攪拌翼の幅(1枚分). 攪拌槽の半径以下に設定する.
            wing_m (float): 攪拌翼の下端から攪拌槽底部までの距離
            wing_angle (float): 攪拌翼の取付角度
            n_wing (int): パドルの枚数
            file_path
        """
        wing_name = f'ag_wing{suffix:04}.stl'
        if file_path is None:
            file_path = os.path.abspath('./')
        wing_path = os.path.join(file_path, wing_name)

        model.begin()
        self.set_partset()
        self.create_wing(wing_h, wing_t, wing_w, wing_m, wing_angle, n_wing)
        model.exportToXAO(
            self.part_docs['wing'], '/tmp/wings.xao',
            model.selection('COMPOUND', 'wings'), 'XAO')
        model.end()

        geompy = geomBuilder.New()
        (imported, wings, [], [], []) = geompy.ImportXAO(
            '/tmp/wings.xao')
        geompy.ExportSTL(wings, wing_path, True, 0.001, True)
model.begin()

は、おまじないです。
GUIでShaperモジュールを選択するのと同じ効果だと思います。
これまでに作成した関数群を実行し、パーツセットを作成、パーツを新規作成、スケッチング、押し出し、回転コピーまで進めます。

model.exportToXAO()

この関数はパーツをXAOファイルに出力します。
次段がXAOファイルが読み書きできるソフトウェアならば、ここで終了です。
私はSTLファイルとして出力したいので、GeometryモジュールにXAOファイルを読み込ませて、STLファイルに変換します。

geomBuilder.New()

でGeometryモジュールのハンドラを取得して、

geompy.ImportXAO()

メソッドを通してShaperで作成したXAOファイルをGeometryモジュール上に取り込みます。 GUI上でExport To Geomボタンを押すのと同じです。

geompy.ExportSTL()

を実行すれば読み込んだモデルをSTLファイルとしてエクスポートできます。 第3引数以降はよく分かりません。

お疲れさまでした。

(おまけ1)単独実行できるようにしておく。

pyファイルの末尾に

if __name__ == 'main':
    paddle = Paddle()
    paddle.export_paddle(hogehoge)

を追加しておきます。
SALOMEをヘッドレスモードで起動し、このファイルを渡せば翼が出力されると思います。

(おまけ2)SALOMEをヘッドレスモードで使う

シェルを開いてSALOMEインストールディレクトリに移動しておく。

./salome -t
./salome shell wing_build.py
./salome killall

-tオプションでSALOMEサーバーをヘッドレスモードで起動します。
2行目でSALOMEのPythonインタプリタへpyファイルを渡しています。
pyファイルに実行時引数を渡したいときは ''' bash ./salome shell wing_build.py args:hoge ''' のように args:をつけてpyファイルの後に置きます。
翼の形状パラメータを流し込むときに使えます。
スクリプトの実行が終わったらkillallコマンドでサーバーをシャットダウンします。