import os
import re
import numpy as np
import openvsp as vsp
import math
XSEC_CRV_TYPE = {
vsp.XS_UNDEFINED : 'XS_UNDEFINED' ,
vsp.XS_POINT : 'XS_POINT' ,
vsp.XS_CIRCLE : 'XS_CIRCLE' ,
vsp.XS_ELLIPSE : 'XS_ELLIPSE' ,
vsp.XS_SUPER_ELLIPSE : 'XS_SUPER_ELLIPSE' ,
vsp.XS_ROUNDED_RECTANGLE : 'XS_ROUNDED_RECTANGLE' ,
vsp.XS_GENERAL_FUSE : 'XS_GENERAL_FUSE' ,
vsp.XS_FILE_FUSE : 'XS_FILE_FUSE' ,
vsp.XS_BICONVEX : 'XS_BICONVEX' ,
vsp.XS_WEDGE : 'XS_WEDGE' ,
vsp.XS_EDIT_CURVE : 'XS_EDIT_CURVE' ,
vsp.XS_FOUR_SERIES : 'XS_FOUR_SERIES' ,
vsp.XS_SIX_SERIES : 'XS_SIX_SERIES' ,
vsp.XS_FILE_AIRFOIL : 'XS_FILE_AIRFOIL' ,
vsp.XS_CST_AIRFOIL : 'XS_CST_AIRFOIL' ,
vsp.XS_VKT_AIRFOIL : 'XS_VKT_AIRFOIL' ,
vsp.XS_FOUR_DIGIT_MOD : 'XS_FOUR_DIGIT_MOD' ,
vsp.XS_FIVE_DIGIT : 'XS_FIVE_DIGIT' ,
vsp.XS_FIVE_DIGIT_MOD : 'XS_FIVE_DIGIT_MOD' ,
vsp.XS_ONE_SIX_SERIES : 'XS_ONE_SIX_SERIES' ,
vsp.XS_NUM_TYPES : 'XS_NUM_TYPES' ,
}
PARM_TYPE = {
vsp.PARM_DOUBLE_TYPE : 'double',
vsp.PARM_INT_TYPE : 'int',
vsp.PARM_BOOL_TYPE : 'boolean',
vsp.PARM_FRACTION_TYPE : 'fraction',
vsp.PARM_LIMITED_INT_TYPE : 'limited int',
vsp.PARM_NOTEQ_TYPE : 'noteq',
vsp.PARM_POWER_INT_TYPE : 'power int',
}
class BuildePlane:
"""
OpenVSP を利用して航空機 (主翼・水平尾翼・垂直尾翼) を構築するクラス。
vsp (openvsp Python API) をラップし、XWIMP 形式の定義ファイルから各翼を生成して
ひとつの .vsp3 モデルとして保存する。
Parameters
----------
wing_file : str, default "wing.xwimp"
主翼 (Main Wing) の定義ファイル (XWIMP 形式) へのパス。
tail_file : str, default "tail.xwimp"
水平尾翼 (Horizontal Tail) の定義ファイルパス。
fin_file : str, default "fin.xwimp"
垂直尾翼 (Vertical Tail / Fin) の定義ファイルパス。
"""
def __init__(self, wing_file="wing.xwimp", tail_file="tail.xwimp", fin_file="fin.xwimp"):
# [重要] 既存の VSP モデルを完全にクリアする。
# - これ以降は新規モデルとして構築される。
# - 未保存の変更は失われるため、外部からの呼び出し順序・保存ロジックを設計する際は注意。
vsp.ClearVSPModel()
# 主翼 (wing) 、水平尾翼 (tail) 、垂直尾翼 (fin) をそれぞれ XWIMP から生成。
# - make_wing_from_xwimp(...) 内で XWIMP の各セクションが追加され、
# スパン/コード/後退角/捩り/上反角/翼型などが設定される。
# - is_vertical=False : 平面 (対称) 翼として初期化。
self.wing_id = self.make_wing_from_xwimp(wing_file, is_vertical=False)
# - 水平尾翼も平面翼として初期化。
self.tail_id = self.make_wing_from_xwimp(tail_file, is_vertical=False)
# - 垂直尾翼は is_vertical=True : 対称面をオフ、X 軸回転で 90 度立てる等の処理を行う。
self.fin_id = self.make_wing_from_xwimp(fin_file, is_vertical=True)
# パラメータ変更を評価して内部ジオメトリを確定。
# - InsertXSec/SetParmValUpdate で設定した値がすべて反映される。
# - 大量のジオメトリを扱う場合は、性能観点で Update の呼び出し回数を最小化すると良い。
vsp.Update()
# 現在のモデル (主翼・尾翼を含む全体) を .vsp3 としてディスクへ保存。
# - 既存ファイルがある場合は上書きされる (ファイルロック・書き込み権限に注意) 。
vsp.WriteVSPFile("aircraft.vsp3")
# ===== パラメータ設定補助 =====
def set_airfoil_xsec(self, xsec_surf_id, xsec_index, crv_type, args):
"""
XSec (翼断面) の形状タイプとパラメータを設定する補助関数。
説明 :
- OpenVSP の XSecSurface (xsec_surf_id) 内の指定インデックス (xsec_index) に対して
断面タイプ (crv_type) を設定し、crv_type に応じたパラメータを args から読み取って反映する。
- この関数自体は vsp.Update() を呼ばないため、呼び出し後に呼び出し元で vsp.Update() を実行して
形状に反映する必要があり。
引数 :
- xsec_surf_id (int): vsp.GetXSecSurf(...) で取得した XSecSurface の ID (整数) 。
- xsec_index (int): XSecSurface 内の断面インデックス (通常 0 始まり) 。
- crv_type (int): vsp の XSEC_CRV_TYPE 定数のいずれか (例 : vsp.XS_FILE_AIRFOIL) 。
- args (dict): crv_type に応じた設定値を持つ辞書。
"""
# 断面タイプを変更する。
# vsp.ChangeXSecShape は xsec_surf_id と断面インデックスに対して、新しい断面タイプを割り当てる。
# 注意 : 既存の断面タイプから変更するとき、一部の内部データ (翼型の座標など) がリセットされる可能性がある。
vsp.ChangeXSecShape(xsec_surf_id, xsec_index, crv_type)
# 変更した断面の XSec ID を取得する。
# 以降、個々のパラメータはこの xsec_id に対して設定する。
xsec_id = vsp.GetXSec(xsec_surf_id, xsec_index)
# 内部ヘルパー : パラメータ名とデフォルト値を受け取り、args から値を取り出して設定する。
# - vsp.GetXSecParm(xsec_id, parm_name) はそのパラメータの ID を返す。
# - vsp.SetParmValUpdate(parm_id, value) を使うことで、値をセットすると同時に内部の依存関係を更新する。
# 可搬性向上のための注意 : parm_id が無効 (None、0、負数など) である可能性があるため、
# 実運用では parm_id の検査を行い、無効な場合は警告を出す実装を追加することを推奨。
def set_parm(xsec_id, parm_name, default):
parm_id = vsp.GetXSecParm(xsec_id, parm_name)
if parm_id is None or parm_id == '':
print(f"Parameter '{parm_name}' not found for xsec {xsec_id}")
return
else:
vsp.SetParmVal(parm_id, args.get(parm_name, default))
# 各断面タイプごとの処理
# ファイルから読み込む場合 : 翼型座標ファイルを読み込む (例: .dat 形式) 。
if crv_type == vsp.XS_FILE_AIRFOIL:
# 'file' キーは必須。パスは相対ではなく絶対パス推奨。
if "file" not in args:
raise ValueError("XS_FILE_AIRFOIL requires 'file' argument")
# ReadFileAirfoil は xsec_id とファイルパスを受け取り、翼型座標をロードする。
vsp.ReadFileAirfoil(xsec_id, args.get("file", ''))
# CST (Class/Shape Transformation) 曲線で定義する場合
elif crv_type == vsp.XS_CST_AIRFOIL:
# nupper/nlower が指定されていなければデフォルトを使用
nupper = args.get("nupper", 6)
nlower = args.get("nlower", 6)
# 上下の CST 係数配列 (長さは nupper/nlower と一致することが望ましい)
ule = args.get("ule", [0.0] * nupper)
lle = args.get("lle", [0.0] * nlower)
# OpenVSP の CST API に配列を渡す
vsp.SetUpperCST(xsec_id, ule)
vsp.SetLowerCST(xsec_id, lle)
# ===== VKT翼型 (擬似実装: GetVKTAirfoilPnts + SetAirfoilPnts) =====
elif crv_type == vsp.XS_VKT_AIRFOIL:
npts = args.get("npts", 121)
alpha = args.get("alpha", 0.0) * math.pi / 180.0 # degree→rad
epsilon = args.get("epsilon", 0.1)
kappa = args.get("kappa", 0.1)
tau = args.get("tau", 10.0) * math.pi / 180.0 # degree→rad
# VKT 点列を取得
xyz_airfoil = vsp.GetVKTAirfoilPnts(npts, alpha, epsilon, kappa, tau)
# --- numpy 配列に変換 ---
xyz_np = np.array([[p.x(), p.y(), p.z()] for p in xyz_airfoil])
# 上下面に分割
half = npts // 2
up_array = xyz_airfoil[:half][::-1]
low_array = xyz_airfoil[half:]
# XS_FILE_AIRFOIL のときしか SetAirfoilPnts は使えないので一度変換
vsp.ChangeXSecShape(xsec_surf_id, xsec_index, vsp.XS_FILE_AIRFOIL)
xsec_id = vsp.GetXSec(xsec_surf_id, xsec_index)
vsp.SetAirfoilPnts(xsec_id, up_array, low_array)
# NACA 4 桁系列相当 : Camber (ピーク凸度) , CamberLoc (位置) , ThickChord (厚み) など
elif crv_type == vsp.XS_FOUR_SERIES:
set_parm(xsec_id, "Camber", 0.02)
set_parm(xsec_id, "CamberLoc", 0.4)
set_parm(xsec_id, "ThickChord", 0.12)
# SharpTEFlag は後縁を鋭くするフラグ (boolean) 。True/False を渡す。
set_parm(xsec_id, "SharpTEFlag", True)
# NACA 5 桁系列相当 : IdealCl (設計揚力係数) 等を使用
elif crv_type == vsp.XS_FIVE_DIGIT:
set_parm(xsec_id, "IdealCl", 0.3)
set_parm(xsec_id, "CamberLoc", 0.15)
set_parm(xsec_id, "ThickChord", 0.12)
set_parm(xsec_id, "SharpTEFlag", True)
# 6 シリーズ相当
elif crv_type == vsp.XS_SIX_SERIES:
set_parm(xsec_id, "IdealCl", 0.3)
set_parm(xsec_id, "ThickChord", 0.12)
set_parm(xsec_id, "SharpTEFlag", True)
# 4 桁修正版
elif crv_type == vsp.XS_FOUR_DIGIT_MOD:
# CamberInputFlag はcambar lineの入力方法を切り替えるフラグ (仕様確認推奨)
set_parm(xsec_id, "CamberInputFlag", 0)
set_parm(xsec_id, "Camber", 0.02)
set_parm(xsec_id, "CamberLoc", 0.4)
set_parm(xsec_id, "ThickChord", 0.12)
set_parm(xsec_id, "SharpTEFlag", True)
# 5 桁修正版
elif crv_type == vsp.XS_FIVE_DIGIT_MOD:
set_parm(xsec_id, "CamberInputFlag", 0)
set_parm(xsec_id, "IdealCl", 0.3)
set_parm(xsec_id, "CamberLoc", 0.15)
set_parm(xsec_id, "ThickChord", 0.12)
set_parm(xsec_id, "SharpTEFlag", True)
# NACA 16 系列
elif crv_type == vsp.XS_ONE_SIX_SERIES:
set_parm(xsec_id, "IdealCl", 0.3)
set_parm(xsec_id, "ThickChord", 0.12)
set_parm(xsec_id, "SharpTEFlag", True)
# 未サポートの断面タイプを受け取った場合は例外を投げる
else:
raise ValueError(f"Unsupported XSEC_CRV_TYPE: {crv_type}")
# ===== NACAコード解析 =====
def parse_naca_code(self, naca_str):
'''
'''
m = re.match(r'NACA(\d{4,6})$', naca_str.replace('-', '').upper())
if not m:
raise ValueError("入力がNACA4〜6字形式になっていません。例: NACA2412, NACA23012, NACA63415")
code = m.group(1)
length = len(code)
if length == 4:
Camber = int(code[0]) / 100.0
CamberLoc = int(code[1]) / 10.0
ThickChord = int(code[2:]) / 100.0
return {'series': 4, 'Camber': Camber, 'CamberLoc': CamberLoc, 'ThickChord': ThickChord}
elif length == 5 and code[0] != '6': # 5-digit
L = int(code[0])
CamberLoc = int(code[1]) / 100 / 2
ThickChord = int(code[3:]) / 100.0
IdealCl = 0.15 * L
return {'series': 5, 'IdealCl': IdealCl, 'CamberLoc': CamberLoc, 'ThickChord': ThickChord}
elif length == 5 or length == 6: # 6 series
series = int(code[0])
IdealCl = int(code[-3]) / 10.0
ThickChord = int(code[-2:]) / 100.0
return {'series': series, 'IdealCl': IdealCl, 'ThickChord': ThickChord}
else:
raise ValueError("NACAコードの桁数が不正です。")
# ===== XWIMPパーサ =====
def parse_xwimp(self, path):
with open(path, "r") as f:
lines = [l.strip() for l in f.readlines() if l.strip()]
name = lines[0]
sec_data = []
for l in lines[1:]:
toks = l.split()
sec_data.append({
"y": float(toks[0]),
"chord": float(toks[1]),
"x_offset": float(toks[2]),
"dihed": float(toks[3]),
"twist": float(toks[4]),
"foil_in": toks[9],
"foil_out": toks[10],
})
return name, sec_data
# ===== 翼生成 =====
def make_wing_from_xwimp(self, path, is_vertical=False):
name, sec_data = self.parse_xwimp(path)
geom_id = vsp.AddGeom("WING")
vsp.SetGeomName(geom_id, name)
xsec_surf_id = vsp.GetXSecSurf(geom_id, 0)
if is_vertical:
vsp.SetParmVal(vsp.FindParm(geom_id, "Sym_Planar_Flag", "Sym"), 0.0)
pid_rot = vsp.FindParm(geom_id, "X_Rel_Rotation", "XForm")
if pid_rot: vsp.SetParmValUpdate(pid_rot, 90.0)
pid_x = vsp.FindParm(geom_id, "X_Rel_Location", "XForm")
vsp.SetParmValUpdate(pid_x, vsp.GetParmVal(pid_x)+sec_data[0]['x_offset'])
num_to_add = max(0, len(sec_data)) - 1
for i in range(1, num_to_add):
vsp.InsertXSec(geom_id, i, vsp.XS_FOUR_SERIES)
for i, (root_sec, tip_sec) in enumerate(zip(sec_data[:-1], sec_data[1:])):
span = tip_sec['y'] - root_sec['y']
sweep_le = np.rad2deg(np.arctan((tip_sec['x_offset'] - root_sec['x_offset']) / (tip_sec['y'] - root_sec['y'])))
xsec_index = i+1
xsec_id = vsp.GetXSec(xsec_surf_id, xsec_index)
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Span'), span)
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Sweep'), sweep_le)
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Twist'), tip_sec['twist'])
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Dihedral'), root_sec['dihed'])
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Tip_Chord'), tip_sec["chord"])
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Root_Chord'), root_sec["chord"])
vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Sec_Sweep_Location'), 0)
foil_name = root_sec["foil_in"] if i == 0 else root_sec["foil_out"]
if foil_name.upper().startswith("NACA"):
args = self.parse_naca_code(foil_name)
if args['series'] == 4:
crv_type = vsp.XS_FOUR_SERIES
elif args['series'] == 5:
crv_type = vsp.XS_FIVE_DIGIT
elif args['series'] == 6:
crv_type = vsp.XS_SIX_SERIES
else:
raise ValueError("対応外の NACA 系列です")
else:
args = {'file': f'{foil_name}.dat'}
crv_type = vsp.XS_FILE_AIRFOIL
self.set_airfoil_xsec(xsec_surf_id, xsec_index-1, crv_type, args)
self.set_airfoil_xsec(xsec_surf_id, xsec_index, crv_type, args)
vsp.Update()
return geom_id
# ===== 実行例 =====
if __name__ == "__main__":
buildplane = BuildePlane()
def print_parm_discription(geom_id):
parm_array = vsp.GetGeomParmIDs( geom_id )
print(f'| {"parm_name":25s}', f'| {"type":<12s}', '| discription |')
print('| --- | --- | --- |')
for parm_id in parm_array:
parm_name = vsp.GetParmName(parm_id)
parm_type = vsp.GetParmType(parm_id)
parm_discription = vsp.GetParmDescript(parm_id)
print(f'| {parm_name:25s}', f'| {PARM_TYPE.get(parm_type, "Unknown"):12s}', '\t|', parm_discription.replace('Default Description', '-'), '|')
geom_id = vsp.AddGeom("WING")
vsp.SetGeomName(geom_id, "WingGeom")
print_parm_discription(geom_id)
def print_parm_discription(geom_id):
parm_array = vsp.GetXSecParmIDs( geom_id )
print(f'| {"parm_name":25s}', f'| {"type":<12s}', '| discription |')
print('| --- | --- | --- |')
for parm_id in parm_array:
parm_name = vsp.GetParmName(parm_id)
parm_type = vsp.GetParmType(parm_id)
parm_discription = vsp.GetParmDescript(parm_id)
print(f'| {parm_name:25s}', f'| {PARM_TYPE.get(parm_type, "Unknown"):12s}', '\t|', parm_discription.replace('Default Description', '-'), '|')
geom_id = vsp.AddGeom("WING")
vsp.SetGeomName(geom_id, "WingGeom")
xsec_surf_id = vsp.GetXSecSurf(geom_id, 0)
xsec_id = vsp.GetXSec(xsec_surf_id, 1)
print_parm_discription(xsec_id)
import os
import stat
import datetime
from pathlib import Path
# COLOR_RESET = "\033[0m"
# COLOR_DIR = "\033[34m"
# COLOR_LINK = "\033[36m"
# COLOR_FILE = "\033[0m"
COLOR_RESET = ""
COLOR_DIR = ""
COLOR_LINK = ""
COLOR_FILE = ""
def enable_windows_ansi():
pass
def format_permissions(mode):
"""ls -l 形式のパーミッション"""
return stat.filemode(mode)
def format_size_bytes(size):
"""バイト表示"""
return str(size)
def format_size_human(size):
"""単位付きサイズ表示"""
for unit in ["B", "K", "M", "G", "T", "P"]:
if size < 1024:
return f"{size}{unit}"
size //= 1024
return f"{size}E"
def format_time(ts):
"""ls -l 互換の時刻表示"""
dt = datetime.datetime.fromtimestamp(ts)
return dt.strftime("%Y-%m-%d %H:%M")
def classify_suffix(entry: Path):
"""ls -F 互換の種別表示"""
if entry.is_symlink():
return "@"
if entry.is_dir():
return "/"
if entry.is_file() and os.access(entry, os.X_OK):
return "*"
if stat.S_ISFIFO(entry.stat().st_mode):
return "|"
if stat.S_ISSOCK(entry.stat().st_mode):
return "="
return ""
class Stats:
def __init__(self):
self.files = 0
self.dirs = 0
def should_show(entry, all_files, dirs_only):
if not all_files and entry.name.startswith("."):
return False
if dirs_only and not entry.is_dir():
return False
return True
def sorted_entries(path, dirsfirst):
try:
entries = list(path.iterdir())
except PermissionError:
return []
if dirsfirst:
return sorted(entries, key=lambda e: (not e.is_dir(), e.name.lower()))
return sorted(entries, key=lambda e: e.name.lower())
def build_columns(entry, options):
"""表示列生成"""
cols = []
try:
st = entry.lstat()
except PermissionError:
return ""
if options["p"]:
cols.append(format_permissions(st.st_mode))
if options["s"]:
cols.append(format_size_bytes(st.st_size))
if options["h"]:
cols.append(format_size_human(st.st_size))
if options["D"]:
cols.append(format_time(st.st_mtime))
return " ".join(cols)
def colorize(name, entry, color):
if not color:
return name
if entry.is_symlink():
return f"{COLOR_LINK}{name}{COLOR_RESET}"
if entry.is_dir():
return f"{COLOR_DIR}{name}{COLOR_RESET}"
return f"{COLOR_FILE}{name}{COLOR_RESET}"
def print_entry(prefix, entry, is_last, options):
connector = "└── " if is_last else "├── "
try:
st = entry.lstat()
except PermissionError:
return
columns = build_columns(entry, options)
name = entry.name
if options["F"]:
name += classify_suffix(entry)
if entry.is_symlink():
try:
target = os.readlink(entry)
name += f" -> {target}"
except OSError:
pass
name = colorize(name, entry, options["color"])
if columns:
print(f"{prefix}{connector}{columns} {name}")
else:
print(f"{prefix}{connector}{name}")
def walk(path, prefix, depth, options, stats):
if options["max_depth"] is not None and depth > options["max_depth"]:
return
entries = [
e for e in sorted_entries(path, options["dirsfirst"])
if should_show(e, options["all_files"], options["dirs_only"])
]
for i, entry in enumerate(entries):
is_last = i == len(entries) - 1
try:
if entry.is_dir():
stats.dirs += 1
else:
stats.files += 1
except PermissionError:
continue
print_entry(prefix, entry, is_last, options)
if entry.is_dir():
extension = " " if is_last else "│ "
walk(entry, prefix + extension, depth + 1, options, stats)
def tree(
path=".",
all_files=False,
max_depth=None,
dirs_only=False,
color=True,
dirsfirst=False,
p=False,
s=False,
h=False,
D=False,
F=False
):
"""
Linux tree 互換表示関数
"""
enable_windows_ansi()
root = Path(path)
if not root.exists():
print("指定パスが存在しません")
return
options = {
"all_files": all_files,
"max_depth": max_depth,
"dirs_only": dirs_only,
"color": color,
"dirsfirst": dirsfirst,
"p": p,
"s": s,
"h": h,
"D": D,
"F": F,
}
stats = Stats()
print(root)
walk(root, "", 1, options, stats)
print()
print(f"{stats.dirs} directories, {stats.files} files")
if __name__ == "__main__":
tree(".", dirsfirst=True, p=False, h=False, D=False, F=False)