個人的な生成プロセスでgemini-2.5-pro-exp-03-25に試しに生成させた紙飛行機シミュレーターです。想像を超えるものが出力されたので、びっくりしてしまいました。私が直接指示したのは”Pythonで紙飛行機の飛行シミュレーター”のみのファーストテイクです。出力されたコードを記載しておきますので、streamlitを扱える方は気楽にお試しください。
# paper_plane_simulator.py
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
# --- 定数 ---
G = 9.81 # 重力加速度 (m/s^2)
RHO = 1.225 # 標準空気密度 (kg/m^3) at sea level
# --- アプリケーションのタイトル ---
st.title("紙飛行機飛行シミュレーター ✈️")
st.write("""
このシミュレーターは、紙飛行機の設計パラメータや初期条件を入力することで、
その飛行軌跡を物理法則に基づいて計算し、可視化します。
""")
# --- パラメータ入力 (サイドバー) ---
st.sidebar.header("パラメータ入力")
# 物理的特性
st.sidebar.subheader("機体特性")
area = st.sidebar.slider("翼面積 (m^2)", min_value=0.005, max_value=0.05, value=0.015, step=0.001, format="%.3f")
mass = st.sidebar.slider("質量 (kg)", min_value=0.001, max_value=0.01, value=0.005, step=0.0005, format="%.4f")
# 初期飛行条件
st.sidebar.subheader("初期条件")
initial_velocity = st.sidebar.slider("初期速度 (m/s)", min_value=3.0, max_value=20.0, value=10.0, step=0.5)
launch_angle_deg = st.sidebar.slider("投射角度 (度)", min_value=0.0, max_value=45.0, value=10.0, step=1.0)
initial_height = st.sidebar.number_input("初期高度 (m)", min_value=0.0, value=1.5, step=0.1)
# 空気力学モデルパラメータ (簡易モデル)
st.sidebar.subheader("空気力学モデル (簡易)")
st.sidebar.write("揚力係数 CL = c1 * alpha")
c1 = st.sidebar.slider("CL係数 c1", min_value=0.1, max_value=4.0, value=2.0, step=0.1, help="揚力係数と迎え角の比例係数。値が大きいほど揚力が大きくなる。")
st.sidebar.write("抗力係数 CD = c0 + c2 * alpha^2")
c0 = st.sidebar.slider("CD係数 c0 (ゼロ揚力抗力)", min_value=0.01, max_value=0.1, value=0.03, step=0.005, format="%.3f", help="迎え角ゼロでの抗力係数(形状抵抗など)。")
c2 = st.sidebar.slider("CD係数 c2 (誘導抗力係数)", min_value=0.01, max_value=0.5, value=0.05, step=0.005, format="%.3f", help="揚力の発生に伴う抗力(誘導抗力)の係数。")
stall_angle_deg = st.sidebar.slider("失速迎え角 (度)", min_value=5.0, max_value=25.0, value=15.0, step=0.5, help="これ以上の迎え角になると揚力が低下し、抗力が増大すると仮定する角度。")
st.sidebar.info("""
**簡易モデルについて:**
- 迎え角 (alpha) は、機体の進行方向と投射角度から簡易的に計算されます。
- 空気力学モデルは非常に単純化されており、実際の飛行とは異なる場合があります。
- **注意:** 適切なパラメータが見つからない場合、飛行軌道が不自然になることがあります。
""")
# --- シミュレーション制御 ---
st.sidebar.subheader("シミュレーション制御")
max_time = st.sidebar.number_input("最大シミュレーション時間 (s)", min_value=1.0, max_value=60.0, value=10.0, step=0.5)
dt = st.sidebar.select_slider(
"時間ステップ dt (s)",
options=[0.001, 0.002, 0.005, 0.01, 0.02, 0.05],
value=0.01,
help="計算の時間刻み幅。小さいほど精度が上がるが、計算時間が長くなる。"
)
# --- シミュレーションエンジン (プレースホルダ) ---
def simulate_flight(params):
# ここにシミュレーションロジックが入る
# 今はダミーデータを返す
x = np.linspace(0, 10, 100)
y = -0.1 * (x - 5)**2 + 3.0
y = np.maximum(y, 0) # 地面以下にはいかない
time_flight = 3.0
max_height = 3.0
distance = 10.0
error_message = None
# ---- !!! ここから物理シミュレーションを実装します !!! ----
# パラメータ展開
area = params['area']
mass = params['mass']
v0 = params['initial_velocity']
angle0_rad = np.radians(params['launch_angle_deg'])
y0 = params['initial_height']
c1 = params['c1']
c0 = params['c0']
c2 = params['c2']
stall_angle_rad = np.radians(params['stall_angle_deg'])
max_t = params['max_time']
dt = params['dt']
# 初期状態
x_pos = [0.0]
y_pos = [y0]
vx = [v0 * np.cos(angle0_rad)]
vy = [v0 * np.sin(angle0_rad)]
t = [0.0]
# オイラー法による数値積分ループ
current_t = 0.0
while current_t < max_t:
# 現在の状態
current_vx = vx[-1]
current_vy = vy[-1]
current_x = x_pos[-1]
current_y = y_pos[-1]
# 地面に到達したら終了
if current_y < 0 and current_t > 0.1: # 最初のステップでの誤判定を防ぐ
# 線形補間で着地位置と時間を推定(簡易版)
t_ratio = -y_pos[-2] / (current_y - y_pos[-2]) if (current_y - y_pos[-2]) != 0 else 0
final_t = t[-2] + t_ratio * dt
final_x = x_pos[-2] + t_ratio * (current_x - x_pos[-2])
# 結果を上書き
t[-1] = final_t
x_pos[-1] = final_x
y_pos[-1] = 0.0
break
# 速度と飛行経路角
v = np.sqrt(current_vx**2 + current_vy**2)
if v < 1e-6: # 速度がゼロに近い場合は計算を打ち切る
error_message = "速度がほぼゼロになりました。"
break
gamma = np.arctan2(current_vy, current_vx) # 飛行経路角 (rad)
# 簡易的な迎え角 alpha の計算
# ここでは非常に簡易的に、機体の姿勢は常に投射角度に固定されていると仮定する
# 実際の飛行では機体姿勢も変化するが、簡単のため省略
# alpha = angle0_rad - gamma
# ----> より簡易的に「機体角度」は初期投射角度のままとする案(pitch_angle = angle0_rad)
alpha = angle0_rad - gamma # これはより現実に近いかも?機体軸と速度ベクトルのなす角
# ----> さらに簡易的に、alphaは飛行経路角 gamma に依存させず、一定と考えるか?
# ----> もしくは、水平に近いと仮定して alpha ≒ 投射角度(pitch angle) とみなす?
# 指示書モデルに基づき、alpha = (投射角度相当) - 飛行経路角 としてみる
# 補正:αが負にならないように調整(常に上向きの揚力を仮定する場合。簡易モデルとして許容?)
# alpha = max(0, angle0_rad - gamma) # これは物理的ではないかも
# --- 再考 ---
# α は 機体基準線 (例:翼弦線) と 空気流の相対角度
# 簡易モデルでは、機体の姿勢角(θ=pitch angle)を一定(=投射角度 angle0_rad)と仮定することが多い
# 空気流の方向は速度ベクトル (-vx, -vy) の方向、つまり -gamma
# よって、alpha = θ - gamma = angle0_rad - gamma が一つの妥当な簡易モデル
# CL, CD の計算 (失速を考慮)
if abs(alpha) <= stall_angle_rad:
cl = c1 * alpha
cd = c0 + c2 * alpha**2
else:
# 失速状態:CLは低下し、CDは増大すると仮定 (簡易モデル)
# 低下・増大の度合いは調整が必要
cl_stall = c1 * stall_angle_rad * 0.7 # 例: 失速角での70%に低下
cd_stall = c0 + c2 * stall_angle_rad**2 + 0.1 # 例: 抗力が大幅増
if alpha > stall_angle_rad:
cl = cl_stall
cd = cd_stall
else: # 負の大きな迎え角の場合
cl = -cl_stall # 負の揚力
cd = cd_stall
# CL/CDの急変は不安定要因になる可能性がある点に注意
# 動圧
q = 0.5 * RHO * v**2
# 揚力(L)と抗力(D)
lift = q * area * cl
drag = q * area * cd
# 力の成分 (x, y)
# 重力
fgx = 0.0
fgy = -mass * G
# 空気力 (Lift/Drag座標系からx/y座標系へ変換)
# Lはvに垂直、Dはvと逆向き
fax = -drag * np.cos(gamma) - lift * np.sin(gamma)
fay = -drag * np.sin(gamma) + lift * np.cos(gamma)
# 合力
fx = fgx + fax
fy = fgy + fay
# 加速度
ax = fx / mass
ay = fy / mass
# 次のステップの速度と位置 (オイラー法)
next_vx = current_vx + ax * dt
next_vy = current_vy + ay * dt
next_x = current_x + current_vx * dt # 微修正: current_vx * dt を使うのが素直
next_y = current_y + current_vy * dt
# データを記録
vx.append(next_vx)
vy.append(next_vy)
x_pos.append(next_x)
y_pos.append(next_y)
t.append(current_t + dt)
# 時間更新
current_t += dt
# 念のための発散防止 (大きなステップなど)
if not (np.isfinite(next_x) and np.isfinite(next_y)):
error_message = "計算が発散しました。時間ステップdtを小さくするか、パラメータを見直してください。"
# 途中までのデータで結果を返す
x_pos = x_pos[:-1]; y_pos = y_pos[:-1]; t = t[:-1]; vx = vx[:-1]; vy = vy[:-1]
break
# 最終的な計算結果の抽出
x_arr = np.array(x_pos)
y_arr = np.array(y_pos)
time_flight = t[-1]
max_height = np.max(y_arr) if len(y_arr) > 0 else y0
distance = x_arr[-1] if len(x_arr) > 0 else 0.0
# ゼロ除算等で NaN になった場合のエラーハンドリング
if np.isnan(time_flight) or np.isnan(max_height) or np.isnan(distance):
error_message = "計算結果が無効な値になりました。パラメータを確認してください。"
# 空の結果を返す
return np.array([]), np.array([]), 0.0, 0.0, 0.0, error_message
return x_arr, y_arr, time_flight, max_height, distance, error_message
# --- シミュレーション実行ボタン ---
if st.sidebar.button("シミュレーション開始"):
# パラメータを辞書にまとめる
params = {
'area': area,
'mass': mass,
'initial_velocity': initial_velocity,
'launch_angle_deg': launch_angle_deg,
'initial_height': initial_height,
'c1': c1,
'c0': c0,
'c2': c2,
'stall_angle_deg': stall_angle_deg,
'max_time': max_time,
'dt': dt
}
# スピナー表示で計算中を示す
with st.spinner('飛行軌跡を計算中...'):
try:
x_traj, y_traj, flight_time, max_h, flight_dist, error_msg = simulate_flight(params)
if error_msg:
st.error(f"シミュレーションエラー: {error_msg}")
elif len(x_traj) == 0:
st.warning("シミュレーション結果が空です。パラメータを確認してください。")
else:
st.subheader("シミュレーション結果")
# 結果の数値表示
col1, col2, col3 = st.columns(3)
col1.metric("飛行時間 (s)", f"{flight_time:.2f}")
col2.metric("最大到達高度 (m)", f"{max_h:.2f}")
col3.metric("水平飛行距離 (m)", f"{flight_dist:.2f}")
# 飛行軌跡グラフ
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(x_traj, y_traj, label="飛行軌跡")
ax.set_xlabel("水平距離 (m)")
ax.set_ylabel("高度 (m)")
ax.set_title("計算された飛行軌跡")
ax.set_aspect('equal', adjustable='box') # アスペクト比を1:1に
ax.grid(True)
# x,yの表示範囲を自動調整しつつ、yは0以上を確保
ax.set_xlim(left=0)
ax.set_ylim(bottom=0)
min_data_bound_x = np.min(x_traj) if len(x_traj) > 0 else 0
max_data_bound_x = np.max(x_traj) if len(x_traj) > 0 else 1
min_data_bound_y = np.min(y_traj) if len(y_traj) > 0 else 0
max_data_bound_y = np.max(y_traj) if len(y_traj) > 0 else 1
ax.set_xlim([min(0, min_data_bound_x-0.5), max_data_bound_x + 0.5])
ax.set_ylim([0, max(max_data_bound_y + 0.5, initial_height + 1.0)]) # 初期高度も見切れないように
ax.legend()
st.pyplot(fig)
except Exception as e:
st.error(f"予期せぬエラーが発生しました: {e}")
# デバッグ用にトレースバックも表示する (開発中のみ)
# import traceback
# st.error(traceback.format_exc())
else:
# 初期表示またはパラメータ変更時
st.info("左のサイドバーでパラメータを調整し、「シミュレーション開始」ボタンを押してください。")
# プレースホルダーグラフ表示(オプション)
fig, ax = plt.subplots()
ax.set_xlabel("水平距離 (m)")
ax.set_ylabel("高度 (m)")
ax.set_title("飛行軌跡 (シミュレーション待機中)")
ax.grid(True)
ax.set_xlim(0, 10)
ax.set_ylim(0, 5)
st.pyplot(fig)
# --- 補足説明 ---
st.markdown("""
---
### モデルの仮定と簡易化
* **2次元モデル**: 飛行は垂直平面内のみで考慮され、横風や旋回はありません。
* **質点モデル**: 機体の形状や回転運動は無視され、質量が一点に集中していると仮定します。
* **空気力学**:
* 揚力係数(CL)は迎え角(α)に比例 (`CL = c1 * α`)。
* 抗力係数(CD)は迎え角の2乗に比例する項を含む (`CD = c0 + c2 * α^2`)。
* **迎え角(α)** は `機体の想定ピッチ角(≒初期投射角度) - 飛行経路角(速度ベクトル角度)` で簡易的に計算しています。これは非常に単純化された仮定です。
* **失速**: 迎え角が一定値を超えると、揚力が低下し抗力が増加する簡易的なモデルを導入しています。
* 空気密度は一定としています。
* **数値計算法**: オイラー法を使用しており、時間ステップ `dt` が大きいと誤差が蓄積する可能性があります。
""")