Ansys Fluentを使っていたときにコマンド量産してた話

以前、環境コンサルの会社でAnsys社のFluentという流体解析ソフトを使って流体のシミュレーションを行っていた。

当時、業務効率化を図って作成したプログラムがあった。シミュレーションの実行および解析結果(数値、画像)を自動化するためのプログラムだ。

作ったときはPython、というかプログラミング言語に触れて数か月だったので色々苦労したが、正規表現のパワフルさやPythonのリストの柔軟性に心奪われたことを時折思い出す。記憶を頼りに、当時作成したプログラムを再現してみる。

あ、もし Fluentユーザーいたら使ってみてね！！

どんなものか、例を交えて説明しようと思う。

風向き(北、東、南、西)それぞれに対して3種類の風速(1m/s, 5m/s, 10m/s)で何らかのシミュレーションを行ったとする。

このとき、以下の擬似コマンドが欲しい。ただし、ハイフン(-)は見やすさのために便宜的に記載しただけで、コマンドではない。

read file simulation_N_01ms.sim

save figure result_01_N_01ms.png

-

read file simulation_N_05ms.sim

save figure result_02_N_05ms.png

-

read file simulation_N_05ms.sim

save figure result_03_N_10ms.png

・

・

read file simulation_W_10ms.sim

save figure result_12_W_10ms.png

これらは、read file でシミュレーションの結果ファイルを読み込み、save figure で画像を保存するようなコマンドである。

一つ目の

read file simulation_N_01ms.sim

save figure result_01_N_01ms.png

に着目して話を進めよう。

シミュレーションの結果ファイルの名前 simulation_N_01ms.sim は規則的につけられており、simulation_N_01ms.sim の N は北を表す。01 は風速を表す。

画像の名前も規則的につけたいとする。result_01_N_01ms.png の 01 は 01 から始まる連番である。N は北を表し、01 は風速を表す。

気持ちをコードで表現すれば、以下である。

```
連番 = 0
for 風向き(北:N, 東:E, 南:S, 西:E):
  for 風速(01m/s, 05m/s, 10m/s):
    シミュレーションの結果ファイルを読み込む # 風向き、風速の情報とファイル名が対応している
    連番 + = 1
    画像を保存 # 風向き、風速、連番の情報を画像名に入れる
```

これをエクセルで実行しようとすると、案外手間である。

そこで、

read file simulation_N_01ms.sim

save figure result_01_N_01ms.png

というコマンド群のうち、値を変えたい箇所を && の2文字で置き換えたコマンド群を用意する。つまり下記だ。

read file simulation_\&\&_\&\&ms.sim

save figure result_\&\&\_\&\&\_\&\&ms.png

ここで、ルールを決めておこう。複製したい処理の単位を'コマンド群'と呼ぶことにする。上記で言えば、2行まとまったものを コマンド群 と呼ぶことにする。

さて、&& で置き換えられたコマンド群は見るからに気持ち悪いが、これは、正規表現の特殊文字を使うと面倒くさいから、という意思に基づいた結果である。

説明を続けよう。このプログラムでは、コマンド群の各行の各&&について、それぞれ

・1次関数 $a n + b$ ($a,b$は整数、ゼロ埋め可能)　例：10,15,20,25,30

・パターンの繰り返し　例：a,b,c,a,b,c,a,b,c

・回数指定のパターン　例：北,北,北,東,東,東,南,南,南,西,西,西

のいずれかで置換することができる。

先ほどのコマンド群の例でみてみよう。まずは再掲。

read file simulation_\&\&_\&\&ms.sim

save figure result_\&\&\_\&\&\_\&\&ms.png

①今回紹介するPythonプログラムを実行すると、コマンド群の入力が求められるので、上記の&&を含むコマンド群を入力する。入力は空白行(改行)によって終了する。

②質問を受ける。「今回、コマンドをいくつ作成する？」→4種類の風向きそれぞれについて3種類の風速があるので、12このコマンド群を作成したい。12 を入力する。

③質問を受ける。「read file simulation_\&\&_\&\&ms.sim の1番目の\&\&は何で置き換える？　1.1次関数、2.パターンの繰り返し、3.回数指定のパターン」→ここは方角を入れたい。「北,北,北,東,東,東,南,南,南,西,西,西」と入力したいので、3.回数指定のパターン が適切。 3 を入力し、パターンの入力画面へ。「北\*3,東\*3,南\*3,西\*3」と入力する。

④質問を受ける。「read file simulation_\&\&_\&\&ms.sim の2番目の\&\&は何で置き換える？　1.1次関数、2.パターンの繰り返し、3.回数指定のパターン」→ここは風速を入れたい。「01,05,10,01,05,10,01,05,10,01,05,10」 と入力したいので、2.パターンの繰り返し が適切。2 を入力し、パターンの繰り返しの入力画面へ。「01,05,10」と入力する。

⑤質問を受ける。「save figure result_\&\&\_\&\&\_\&\&ms.png の1番目の\&\&は何で置き換える？　1.1次関数、2.パターンの繰り返し、3.回数指定のパターン」→ここは連番を入れたい。「01,02,03,04,05,06,07,08,09,10,11,12」と入力したいので、1.1次関数 が適切。 1 を入力し、1次関数のパラメータの入力画面へ。$y = 1 n + 1 (n ≧ 1)$ となるようパラメータを入力する。パラメータ入力後は、ゼロ埋めするか質問を受ける。質問に沿ってゼロ埋めの桁数 2 を入力する。

・

・

・

という感じで質問への回答をすべて終えると、標準出力で変換後のコマンドたちが出てくる。


プログラムは以下。

```
import re
import math

pattern = re.compile(r'&&')
# lines: オリジナルのコマンド群を保存するリスト
lines = []

print('コマンドを量産します。可変部を && (前後の空白)で置き換えたコマンドを入力してください。\n空白行をもって入力を終了します。')
while True:
    line = input()
    lines.append(line)
    if line == '':
        break

while True:
    num_raw = input('コマンド群はいくつ作成しますか？半角の自然数を入力してください\n')
    try:
        # num: コマンドの量産数
        num = int(num_raw)
    except ValueError:
        print('invalid value')
    else:
        break

# transforms: オリジナルのコマンド群をnum個ずつ複製しておくリスト
# transforms内のコマンドを漸次変換していく。
transforms = [[line] * num for line in lines]

# && にマッチした行については置換処理を走らせる。
# i: コマンド群の中での行インデックス
def execute_transform(i):
    # matched_num: 行内の && の数
    matched_num = len(pattern.findall(lines[i]))
    for j in range(matched_num):
        # 行内j番目の && の置換を実施する。
        while True:
            print(f'\n" {lines[i]} "の{j+1}番目の && を置換します。\n')
            print('1.1次関数 y = an + b で置換しますか？ ex.10,12,14, .. \n2.パターンの繰り返しで置換しますか？ ex.a,b,c \n3.回数指定のパターンで置換しますか？ ex.A*2,B*3,C*2')
            question = input('1,2,3 のいずれかを半角数字で入力してください\n')
            # 行内の先頭の&&だけを置換する3つの関数
            if question == "1":
                set_linear_function(i)
                break
            if question == "2":
                set_simple_repeat(i)
                break
            if question == "3":
                set_complex_repeat(i)
                break
            else:
                print('1,2,3のいずれかを半角英数字で入力してください。')
                continue
        # 毎回の変換の過程が見たければ以下をコメントアウトすればいい
        # print(transforms[i], end='\n\n')

# set_linear_function: 1次関数で変換する関数(戻り値なし)
def set_linear_function(i):
    print('1次関数 y = an + b で置換します。 ex.10,12,14, ..')
    # 基本的に整数で扱うものとするが、実装を変えて実数とするのもアリ
    while True:
        a = input('傾き a を半角数字で入力してください。\n')
        b = input('切片 b を半角数字で入力してください。\n')
        n = input('n の最小値を指定してください。\n')
        try:
            a = int(a)
            b = int(b)
            n = int(n)
        except ValueError:
            print('invalid value!\n')
            continue
        else:
            y_or_n = input(f'y = {a} n + {b} (n >= {n}) で置換します。続行する場合はEnterを、やり直す場合は n を押下してください\n').strip()
            if y_or_n != 'n':
                break
            else:
                continue
            
    # ゼロ埋めの確認を行い、コマンドの群の変換まで行う
    while True:
        # ゼロ埋めしないものをデフォルトとして進める(自動で桁数を算出するのは手間)
        # 'y' 以外であれば、ゼロ埋めしない
        zero_fill = input('ゼロ埋めしますか？ y or n\n').strip()
        if zero_fill == 'y':
            digit_num = input('桁数を半角数字で入力してください\n')
            try:
                digit_num = int(digit_num)
            except ValueError:
                print('invalid value!')
                continue
            else:
                # 1次関数に従う要素数numのリストを作る
                new_vars = [str(a * n + b).zfill(digit_num) for n in range(n, n + num)]
                transforms[i] = [pattern.sub(str(new_var), line, count=1) for line, new_var in zip(transforms[i], new_vars)]
                break
        else:
            y_or_n = input('ゼロ埋めしません。続行する場合はEnterを、やり直す場合は n を入力してください。\n')
            if y_or_n != 'n':
                # 1次関数に従う要素数numのリストを作る
                new_vars = [a * n + b for n in range(n, n + num)]
                transforms[i] = [pattern.sub(str(new_var), line, count=1) for line, new_var in zip(transforms[i], new_vars)]
                break
            else:
                continue    
    return None

# set_simple_repeat: パターンの繰り返しで変換する関数(戻り値なし)
def set_simple_repeat(i):
    print('パターンの繰り返しで置換します。ex.a,b,c')
    # 空白がある場合は空白をそのまま残す
    while True:
        pat_csv = input('カンマ区切りでパターンを入力してください。(空白不要)\n')
        pat_list = pat_csv.split(',')
        y_or_n = input(f'パターン {pat_list} の繰り返しで置換します。続行する場合はEnterを、やり直す場合は n を押下してください\n').strip()
        if y_or_n != 'n':
            break
        else:
            continue
    # パターンから計num個の配列を作る。
    new_vars = pat_list * math.ceil(num / len(pat_list))
    transforms[i] = [pattern.sub(str(new_var), line, count=1) for line, new_var in zip(transforms[i], new_vars)]
    return None

# set_simple_repeat: 回数指定のパターンで変換する関数(戻り値なし)
# 指定された回数の合計がnumよりも小さい場合は先頭から循環させる
def set_complex_repeat(i):
    print('回数指定のパターンで置換します。 ex.A*2,B*3,C*2')
    # repeat_pattern: カンマ区切りで入力される回数指定のパターンの書式の正規表現
    repeat_pattern = re.compile(r'.*\*\d+')
    while True:
        pat_csv = input('*で回数を指定し、カンマ区切りでパターンを入力してください。(空白不要) \nex. A*2,B*3,C*2\n繰り返し回数がコマンドの量産数を下回る場合、指定されたパターンがはじめから循環します。\n')        
        pat_lists = pat_csv.split(',')
        # 入力されたすべてのパターンが適切かどうかで条件分岐
        if all(map(lambda x: repeat_pattern.match(x), pat_lists)):
            y_or_n = input(f'パターン {pat_lists} の繰り返しで置換します。続行する場合はEnterを、やり直す場合は n を押下してください\n').strip()
            if y_or_n != 'n':
                break
            else:
                continue
        else:
            print('invalid value')
            continue
    # パターンから計num個の配列を作る。
    new_vars_raw = []
    for pat_list in pat_lists:
        pat, repeat_num = pat_list.split('*')
        new_vars_raw += [pat] * int(repeat_num)
    new_vars = new_vars_raw * math.ceil(num / len(new_vars_raw))
    transforms[i] = [pattern.sub(str(new_var), line, count=1) for line, new_var in zip(transforms[i], new_vars)]
    return None

# 全体の処理
for i in range(len(lines)):
    execute_transform(i)

'''
下記の内法表記は、こんな気持ち
for i in range(num):
  for j in range(len(transforms)):
    print(transforms[j][i])
'''
# transforms[i]にはコマンド群のi行目のコマンドがnum個格納されている
# 行と列を入れ替えて、transforms[i]には変換後コマンド群の第i番目が格納されるようにする
transforms_transposed = [transforms[j][i] for i in range(num) for j in range(len(transforms))]
for line in transforms_transposed:
    print(line)

# .pyファイルをそのまま実行する場合はウィンドウがすぐ閉じないように
input()

'''
コマンド例：
read file simulation_&&_&&ms.sim
save figure result_&&_&&_&&ms.png
'''
```

コードの最後に、上述したコマンドの例が記載してあるので、もし使いたい人がいるのならばぜひ試して遊んでほしい。

いくつか、注意点を。

・このプログラムでは、「続行する場合はEnterを、やり直す場合は n を入力してください」のような確認が幾度となくなされる。

この際、やり直す場合に比べて続行する場合が多いと推測し、入力が n 以外であれば続行する仕様になっている。適宜改変可能である。

・1次関数に関しては、$y = a n + b$ の $a,b$ について整数の仮定を置いている。実数にすると桁数に関する質問及び入力が必須になって面倒くさそうだからだ。元をただせば、前職では整数で済んだ、という話なのだが。

・コマンド群の中に空白行がある場合はそこで入力が終了し、後半部分が処理されない。

・Google ColabやJupyter Notebookよりも、Visual Studio CodeやIDLEによる実行がおすすめ。複数行の入力ができるからだ。

・当時はコマンド量産作業をするときは、IDLEでプログラムを開いてからF5で実行していた。IDLEのデザインが好きで、、一応、.pyファイルとしてダブルクリック等で実行した場合に、結果出力後にウィンドウがすぐに閉じないよう、最後にinput()を置いてある。

・コマンド群の中の各行が必ずしも&&を含む必要はない。今回は省スペースのために&&を含む行のみからなるコマンド群で使用例を紹介したが、&&を含まない行は変換前後で何一つ変わらないだけである。