공기역학을 공부하거나 항공·기계 분야 엔지니어링 프로젝트를 진행하다 보면 마하 수에 따른 유동 파라미터를 반복적으로 계산해야 하는 상황이 자주 생긴다. 교재 뒤편에 붙어 있는 수치표를 일일이 뒤적이거나 공식을 직접 계산기에 두드리는 번거로움을 없애기 위해, 이번에 Python으로 GUI 기반 압축성 유동 계산기 pyroCOMP를 직접 만들어 보았다.
pyroCOMP란?
pyroCOMP는 압축성 유동(Compressible Flow)의 네 가지 핵심 케이스를 한 화면에서 계산할 수 있는 데스크톱 애플리케이션이다. 마하 수(M)와 비열비(γ)만 입력하면 관련된 모든 유동 파라미터를 즉시 계산하고, Mach 수 범위 전체에 걸친 그래프까지 실시간으로 보여준다.

사용한 라이브러리는 세 가지입니다.
- tkinter / ttk — GUI 프레임워크 (Python 기본 내장)
- NumPy — 수치 계산 및 벡터 연산
- Matplotlib (TkAgg 백엔드) — 유동 파라미터 그래프 시각화
구현된 네 가지 유동 모델
1. 등엔트로피 유동 (Isentropic Flow)
마찰과 열전달이 없는 가장 기본적인 압축성 유동이다. 정체 압력비(P₀/P), 정체 온도비(T₀/T), 밀도비(ρ₀/ρ), 면적비(A/A*) 등 7개 파라미터를 동시에 출력한다.
def isentropic(M, γ):
t = 1 + (γ-1)/2 * M**2
P = t ** (-γ/(γ-1))
T = t ** (-1)
rho = t ** (-1/(γ-1))
A = (1/M) * (2/(γ+1) * t) ** ((γ+1)/(2*(γ-1)))
return {"P₀/P": 1/P, "T₀/T": 1/T, "ρ₀/ρ": 1/rho, "A/A*": A, ...}
2. 패노 유동 (Fanno Flow) — 마찰이 있는 단열 유동
일정한 단면적 덕트(duct) 내에서 벽면 마찰만 작용할 때의 유동이다. 임계 조건(*)에 대한 압력비(P/P*), 온도비(T/T*), 최대 마찰 파라미터(4fL*/D) 등 6개 값을 계산한다. 마하 수 1에서 음속(sonic) 조건이 되므로, 그래프상 M=1 위치에 기준선을 표시하여 아음속/초음속 구분을 직관적으로 확인할 수 있다.
3. 레일리 유동 (Rayleigh Flow) — 열전달이 있는 유동
마찰 없이 열만 추가(또는 제거)될 때의 유동이다. 연소실, 열교환기 설계에서 중요한 케이스로, 압력비(P/P*), 온도비(T/T*), 정체 온도비(T₀/T₀*) 등을 계산한다.
4. 수직 충격파 (Normal Shock)
초음속 유동에서 발생하는 수직 충격파 전후 관계식입니다. M₁ ≥ 1 조건을 강제 검증하고, 하류 마하 수(M₂), 압력비(P₂/P₁), 온도비(T₂/T₁), 총압비(P₀₂/P₀₁) 등 6개 파라미터를 계산합니다.
def normal_shock(M1, γ):
M2_sq = (M1**2 + 2/(γ-1)) / (2*γ/(γ-1)*M1**2 - 1)
P2 = (2*γ*M1**2 - (γ-1)) / (γ+1)
P02_P01 = (((γ+1)*M1**2/(2+(γ-1)*M1**2))**(γ/(γ-1)) *
((γ+1)/(2*γ*M1**2-(γ-1)))**(1/(γ-1)))
...
M=2 기준 검증 결과: M₂ = 0.5774, P₂/P₁ = 4.5000으로 표준 공기역학 수치표와 정확히 일치한다.
마치며
약 570줄의 Python 코드로 등엔트로피, 패노, 레일리, 수직 충격파 네 가지 압축성 유동을 모두 커버하는 계산기를 만들었다. 공식 자체는 표준 공기역학 교재에 수록된 것이므로 신뢰성 검증도 간단하다. 압축성 유동을 자주 다루는 분들께 실용적인 도구가 되길 바란다.
import tkinter as tk
from tkinter import ttk, messagebox
import numpy as np
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.ticker as ticker
# ── Color Palette ──────────────────────────────────────────────
C = {
"primary": "#0F6E56",
"forest": "#3B6D11",
"dark_bg": "#1A2A24",
"light_bg": "#F2F8F5",
"white": "#FFFFFF",
"text": "#1A2A24",
"muted": "#5A7A6E",
"border": "#C5DDD5",
"accent": "#0F6E56",
"row_alt": "#E6F2EC",
"header_fg": "#FFFFFF",
"btn_hover": "#0A5240",
"entry_bg": "#FFFFFF",
"error": "#C0392B",
}
FONT_UI = ("Segoe UI", 10)
FONT_UI_B = ("Segoe UI", 10, "bold")
FONT_H = ("Segoe UI", 13, "bold")
FONT_NUM = ("Consolas", 10)
FONT_NUM_B= ("Consolas", 10, "bold")
FONT_SMALL= ("Segoe UI", 8)
γ_default = 1.4
# ══════════════════════════════════════════════════════════════
# FLOW EQUATIONS
# ══════════════════════════════════════════════════════════════
def isentropic(M, γ):
"""Returns dict of isentropic ratios for given Mach number."""
t = 1 + (γ-1)/2 * M**2
P = t ** (-γ/(γ-1))
T = t ** (-1)
rho= t ** (-1/(γ-1))
A = (1/M) * (2/(γ+1) * t) ** ((γ+1)/(2*(γ-1)))
return {
"P₀/P": 1/P,
"T₀/T": 1/T,
"ρ₀/ρ": 1/rho,
"A/A*": A,
"P/P₀": P,
"T/T₀": T,
"ρ/ρ₀": rho,
}
def fanno(M, γ):
"""Fanno flow (adiabatic with friction) ratios."""
t = 1 + (γ-1)/2 * M**2
P = (1/M) * np.sqrt((γ+1)/(2*t))
T = (γ+1)/(2*t)
rho= (1/M) * np.sqrt(2*t/(γ+1))
P0 = (1/M) * (2/(γ+1) * t) ** ((γ+1)/(2*(γ-1)))
V = M * np.sqrt((γ+1)/(2*t))
fLmax = (1-M**2)/(γ*M**2) + (γ+1)/(2*γ) * np.log((γ+1)*M**2/(2*t))
return {
"P/P*": P,
"T/T*": T,
"ρ/ρ*": rho,
"P₀/P₀*": P0,
"V/V*": V,
"4fL*/D": fLmax,
}
def rayleigh(M, γ):
"""Rayleigh flow (with heat transfer) ratios."""
t = 1 + γ*M**2
P = (1+γ) / t
T = M**2 * ((1+γ)/t)**2
rho= 1 / (M**2 * (1+γ)/t)
P0_num = (1+γ)/(t) * (2/(γ+1) * (1 + (γ-1)/2 * M**2)) ** (γ/(γ-1))
T0 = 2*γ*M**2 * (1 + (γ-1)/2 * M**2) / ((1+γ*M**2)**2) * (2/(γ+1)) * ((γ+1)/2)
T0_rat = 2*(γ+1)*M**2 / (1+γ*M**2)**2 * (1 + (γ-1)/2 * M**2)
return {
"P/P*": P,
"T/T*": T,
"ρ/ρ*": rho,
"P₀/P₀*": P0_num,
"T₀/T₀*": T0_rat,
}
def normal_shock(M1, γ):
"""Normal shock relations for upstream Mach M1."""
if M1 < 1.0:
return None
M2_sq = (M1**2 + 2/(γ-1)) / (2*γ/(γ-1)*M1**2 - 1)
M2 = np.sqrt(M2_sq)
P2 = (2*γ*M1**2 - (γ-1)) / (γ+1)
T2 = (2*γ*M1**2 - (γ-1)) * (2 + (γ-1)*M1**2) / ((γ+1)**2 * M1**2)
rho2= (γ+1)*M1**2 / (2 + (γ-1)*M1**2)
P02_P01 = (((γ+1)*M1**2/(2+(γ-1)*M1**2))**(γ/(γ-1)) *
((γ+1)/(2*γ*M1**2-(γ-1)))**(1/(γ-1)))
P02_P1 = P02_P01 * (1 + (γ-1)/2*M1**2)**(γ/(γ-1))
return {
"M₂": M2,
"P₂/P₁": P2,
"T₂/T₁": T2,
"ρ₂/ρ₁": rho2,
"P₀₂/P₀₁": P02_P01,
"P₀₂/P₁": P02_P1,
}
# ══════════════════════════════════════════════════════════════
# MAIN APPLICATION
# ══════════════════════════════════════════════════════════════
class pyroCOMPApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("pyroCOMP")
self.configure(bg=C["light_bg"])
self.resizable(True, True)
self._build_styles()
self._build_ui()
self.minsize(860, 620)
self.protocol("WM_DELETE_WINDOW", self._on_close)
self._center_window(860, 620)
def _center_window(self, w, h):
self.update_idletasks()
sw = self.winfo_screenwidth()
sh = self.winfo_screenheight()
x = (sw - w) // 2
y = (sh - h) // 2
self.geometry(f"{w}x{h}+{x}+{y}")
def _on_close(self):
plt.close("all")
self.quit()
self.destroy()
# ── ttk styles ────────────────────────────────────────────
def _build_styles(self):
s = ttk.Style(self)
s.theme_use("clam")
s.configure("TNotebook",
background=C["light_bg"],
borderwidth=0,
tabmargins=[2, 4, 2, 0])
s.configure("TNotebook.Tab",
font=FONT_UI_B,
background=C["border"],
foreground=C["text"],
padding=[16, 6],
borderwidth=0)
s.map("TNotebook.Tab",
background=[("selected", C["primary"]),
("active", C["muted"])],
foreground=[("selected", C["white"]),
("active", C["white"])])
s.configure("TFrame", background=C["light_bg"])
s.configure("TLabel", background=C["light_bg"],
foreground=C["text"], font=FONT_UI)
s.configure("TEntry", fieldbackground=C["entry_bg"],
foreground=C["text"], font=FONT_NUM,
borderwidth=1, relief="solid")
s.configure("TCombobox", fieldbackground=C["entry_bg"],
background=C["border"],
foreground=C["text"], font=FONT_UI,
arrowcolor=C["primary"])
s.map("TCombobox",
fieldbackground=[("readonly", C["entry_bg"])],
selectbackground=[("readonly", C["primary"])],
selectforeground=[("readonly", C["white"])])
s.configure("Calc.TButton",
font=FONT_UI_B,
background=C["primary"],
foreground=C["white"],
borderwidth=0,
padding=[20, 8],
relief="flat")
s.map("Calc.TButton",
background=[("active", C["btn_hover"]),
("pressed", C["dark_bg"])])
s.configure("Green.TRadiobutton",
background=C["light_bg"],
foreground=C["text"],
font=FONT_UI)
s.map("Green.TRadiobutton",
background=[("active", C["light_bg"])],
indicatorcolor=[("selected", C["primary"])])
s.configure("Header.TLabel",
background=C["dark_bg"],
foreground=C["white"],
font=FONT_H,
padding=[18, 10])
s.configure("Treeview",
background=C["white"],
fieldbackground=C["white"],
foreground=C["text"],
font=FONT_NUM,
rowheight=26,
borderwidth=0)
s.configure("Treeview.Heading",
background=C["primary"],
foreground=C["white"],
font=FONT_UI_B,
relief="flat")
s.map("Treeview",
background=[("selected", C["primary"])],
foreground=[("selected", C["white"])])
s.configure("TSeparator", background=C["border"])
# ── top bar ───────────────────────────────────────────────
def _build_ui(self):
# Header
hdr = tk.Frame(self, bg=C["dark_bg"], height=48)
hdr.pack(fill="x")
tk.Label(hdr, text="⬡ pyroCOMP", font=("Segoe UI", 14, "bold"),
bg=C["dark_bg"], fg=C["white"]).pack(side="left", padx=18, pady=8)
tk.Label(hdr, text="Compressible Flow Calculator",
font=("Segoe UI", 9), bg=C["dark_bg"],
fg=C["muted"]).pack(side="left", pady=8)
# Notebook
nb = ttk.Notebook(self)
nb.pack(fill="both", expand=True, padx=12, pady=10)
self.tabs = {}
tabs_cfg = [
("Isentropic", IsentropicTab),
("Friction", FannoTab),
("Heat Transfer", RayleighTab),
("Normal Shock", NormalShockTab),
]
for name, cls in tabs_cfg:
frame = cls(nb)
nb.add(frame, text=f" {name} ")
self.tabs[name] = frame
# Status bar
self.status = tk.StringVar(value="Ready")
sb = tk.Frame(self, bg=C["border"], height=22)
sb.pack(fill="x", side="bottom")
tk.Label(sb, textvariable=self.status,
bg=C["border"], fg=C["muted"],
font=FONT_SMALL, anchor="w").pack(side="left", padx=10)
# ══════════════════════════════════════════════════════════════
# BASE TAB
# ══════════════════════════════════════════════════════════════
class BaseTab(ttk.Frame):
FLOW_NAME = "Flow"
PARAMS = [] # list of output param names
GRAPH_TITLE = ""
def __init__(self, parent):
super().__init__(parent)
self.γ_var = tk.DoubleVar(value=γ_default)
self.M_var = tk.StringVar(value="1.0")
self.flow_var= tk.StringVar(value="subsonic")
self._build()
# ── layout skeleton ───────────────────────────────────────
def _build(self):
self.columnconfigure(0, weight=0, minsize=320)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
# ── LEFT PANEL ──
left = ttk.Frame(self, style="TFrame")
left.grid(row=0, column=0, sticky="nsew", padx=(10,4), pady=10)
left.columnconfigure(1, weight=1)
# Title banner
ttk.Label(left, text=self.FLOW_NAME,
style="Header.TLabel").grid(
row=0, column=0, columnspan=2, sticky="ew", pady=(0,12))
# γ
ttk.Label(left, text="Specific Heat Ratio γ").grid(
row=1, column=0, sticky="w", padx=8, pady=4)
γ_e = ttk.Entry(left, textvariable=self.γ_var, width=8)
γ_e.grid(row=1, column=1, sticky="w", padx=8, pady=4)
# Supersonic / Subsonic radio (optional)
rf = ttk.Frame(left)
rf.grid(row=2, column=0, columnspan=2, sticky="w", padx=8, pady=2)
self._add_radios(rf)
# Mach input
ttk.Label(left, text="Upstream Mach Number M").grid(
row=3, column=0, sticky="w", padx=8, pady=(10,4))
M_e = ttk.Entry(left, textvariable=self.M_var, width=12)
M_e.grid(row=3, column=1, sticky="w", padx=8, pady=(10,4))
# Extra controls (subclass)
self._extra_controls(left, start_row=4)
# Calculate button
btn = ttk.Button(left, text="Calculate ▶",
style="Calc.TButton",
command=self._calculate)
btn.grid(row=10, column=0, columnspan=2, pady=14, padx=8, sticky="ew")
# Results table
ttk.Separator(left, orient="horizontal").grid(
row=11, column=0, columnspan=2, sticky="ew", pady=4)
ttk.Label(left, text="Results", font=FONT_UI_B,
foreground=C["primary"]).grid(
row=12, column=0, columnspan=2, sticky="w", padx=8)
self.tree = ttk.Treeview(left, columns=("param","value"),
show="headings", height=len(self.PARAMS)+1)
self.tree.heading("param", text="Parameter")
self.tree.heading("value", text="Value")
self.tree.column("param", width=140, anchor="w")
self.tree.column("value", width=130, anchor="e")
self.tree.grid(row=13, column=0, columnspan=2,
sticky="nsew", padx=8, pady=4)
left.rowconfigure(13, weight=1)
# ── RIGHT PANEL (graph) ──
right = ttk.Frame(self)
right.grid(row=0, column=1, sticky="nsew", padx=(4,10), pady=10)
right.rowconfigure(1, weight=1)
right.columnconfigure(0, weight=1)
ttk.Label(right, text=self.GRAPH_TITLE,
font=FONT_UI_B, foreground=C["primary"]).grid(
row=0, column=0, sticky="w", padx=4, pady=(0,4))
self.fig, self.ax = plt.subplots(figsize=(5.5, 4.8))
self.fig.patch.set_facecolor(C["light_bg"])
self.ax.set_facecolor(C["light_bg"])
self._style_ax()
self.canvas = FigureCanvasTkAgg(self.fig, master=right)
self.canvas.get_tk_widget().grid(
row=1, column=0, sticky="nsew")
right.rowconfigure(1, weight=1)
self._draw_graph(γ_default)
def _add_radios(self, frame):
ttk.Radiobutton(frame, text="Supersonic", variable=self.flow_var,
value="supersonic", style="Green.TRadiobutton").pack(side="left", padx=(0,12))
ttk.Radiobutton(frame, text="Subsonic", variable=self.flow_var,
value="subsonic", style="Green.TRadiobutton").pack(side="left")
def _extra_controls(self, parent, start_row): pass
def _style_ax(self):
self.ax.spines[["top","right"]].set_visible(False)
self.ax.spines[["left","bottom"]].set_color(C["border"])
self.ax.tick_params(colors=C["muted"], labelsize=8)
self.ax.yaxis.label.set_color(C["muted"])
self.ax.xaxis.label.set_color(C["muted"])
self.ax.title.set_color(C["text"])
self.fig.tight_layout(pad=1.5)
def _calculate(self):
try:
M = float(self.M_var.get())
γ = float(self.γ_var.get())
assert M > 0 and γ > 1
except Exception:
messagebox.showerror("Input Error",
"Please enter valid values:\n M > 0\n γ > 1")
return
results = self._compute(M, γ)
if results is None:
messagebox.showerror("Error",
"Calculation failed. Check input range.")
return
self._populate_tree(results)
self._draw_graph(γ)
def _populate_tree(self, results: dict):
for row in self.tree.get_children():
self.tree.delete(row)
for i, (k, v) in enumerate(results.items()):
tag = "alt" if i % 2 else "norm"
self.tree.insert("", "end", values=(k, f"{v:.6f}"), tags=(tag,))
self.tree.tag_configure("alt", background=C["row_alt"])
self.tree.tag_configure("norm", background=C["white"])
def _compute(self, M, γ): raise NotImplementedError
def _draw_graph(self, γ): raise NotImplementedError
# ══════════════════════════════════════════════════════════════
# ISENTROPIC TAB
# ══════════════════════════════════════════════════════════════
class IsentropicTab(BaseTab):
FLOW_NAME = "Isentropic Flow"
GRAPH_TITLE = "Isentropic Ratios vs Mach Number"
PARAMS = ["P₀/P","T₀/T","ρ₀/ρ","A/A*","P/P₀","T/T₀","ρ/ρ₀"]
def _compute(self, M, γ):
return isentropic(M, γ)
def _draw_graph(self, γ):
self.ax.clear()
M = np.linspace(0.01, 5, 500)
colors = [C["primary"], C["forest"], "#E07B39",
"#5B8DD9", "#9B59B6", C["muted"], "#2ECC71"]
labels = ["P₀/P", "T₀/T", "ρ₀/ρ", "A/A*"]
fns = [
lambda M: (1+(γ-1)/2*M**2)**(γ/(γ-1)),
lambda M: 1+(γ-1)/2*M**2,
lambda M: (1+(γ-1)/2*M**2)**(1/(γ-1)),
lambda M: (1/M)*((2/(γ+1))*(1+(γ-1)/2*M**2))**((γ+1)/(2*(γ-1))),
]
for i, (fn, lbl) in enumerate(zip(fns, labels)):
vals = np.clip(fn(M), 0, 20)
self.ax.plot(M, vals, color=colors[i], lw=1.8, label=lbl)
self.ax.axvline(1.0, color=C["muted"], ls="--", lw=1, alpha=0.6)
self.ax.set_xlim(0, 5); self.ax.set_ylim(0, 12)
self.ax.set_xlabel("Mach Number M"); self.ax.set_ylabel("Ratio")
self.ax.legend(fontsize=8, framealpha=0.7,
facecolor=C["light_bg"], edgecolor=C["border"])
self.ax.set_title("Isentropic Flow", fontsize=9)
self._style_ax()
self.canvas.draw()
# ══════════════════════════════════════════════════════════════
# FANNO TAB
# ══════════════════════════════════════════════════════════════
class FannoTab(BaseTab):
FLOW_NAME = "Adiabatic Flow With Friction (Fanno)"
GRAPH_TITLE = "Fanno Flow Ratios vs Mach Number"
PARAMS = ["P/P*","T/T*","ρ/ρ*","P₀/P₀*","V/V*","4fL*/D"]
def _compute(self, M, γ):
return fanno(M, γ)
def _draw_graph(self, γ):
self.ax.clear()
Ms = np.linspace(0.05, 5, 500)
Ms = Ms[Ms != 1.0]
data = {
"P/P*": lambda M: (1/M)*np.sqrt((γ+1)/(2*(1+(γ-1)/2*M**2))),
"T/T*": lambda M: (γ+1)/(2*(1+(γ-1)/2*M**2)),
"P₀/P₀*": lambda M: (1/M)*((2/(γ+1))*(1+(γ-1)/2*M**2))**((γ+1)/(2*(γ-1))),
}
colors = [C["primary"], C["forest"], "#E07B39"]
for (lbl, fn), col in zip(data.items(), colors):
vals = np.clip(fn(Ms), 0, 15)
self.ax.plot(Ms, vals, color=col, lw=1.8, label=lbl)
self.ax.axvline(1.0, color=C["muted"], ls="--", lw=1, alpha=0.6)
self.ax.set_xlim(0, 5); self.ax.set_ylim(0, 8)
self.ax.set_xlabel("Mach Number M"); self.ax.set_ylabel("Ratio")
self.ax.legend(fontsize=8, framealpha=0.7,
facecolor=C["light_bg"], edgecolor=C["border"])
self.ax.set_title("Fanno Flow", fontsize=9)
self._style_ax()
self.canvas.draw()
# ══════════════════════════════════════════════════════════════
# RAYLEIGH TAB
# ══════════════════════════════════════════════════════════════
class RayleighTab(BaseTab):
FLOW_NAME = "Non-Adiabatic Flow With Heat Transfer (Rayleigh)"
GRAPH_TITLE = "Rayleigh Flow Ratios vs Mach Number"
PARAMS = ["P/P*","T/T*","ρ/ρ*","P₀/P₀*","T₀/T₀*"]
def _compute(self, M, γ):
return rayleigh(M, γ)
def _draw_graph(self, γ):
self.ax.clear()
Ms = np.linspace(0.05, 4, 500)
Ms = Ms[Ms != 1.0]
def safe(fn, M):
v = fn(M)
return np.clip(v, 0, 15)
plots = [
("P/P*", lambda M: (1+γ)/(1+γ*M**2), C["primary"]),
("T/T*", lambda M: M**2*((1+γ)/(1+γ*M**2))**2, C["forest"]),
("T₀/T₀*", lambda M: 2*(γ+1)*M**2/(1+γ*M**2)**2*(1+(γ-1)/2*M**2),
"#E07B39"),
]
for lbl, fn, col in plots:
self.ax.plot(Ms, safe(fn, Ms), color=col, lw=1.8, label=lbl)
self.ax.axvline(1.0, color=C["muted"], ls="--", lw=1, alpha=0.6)
self.ax.set_xlim(0, 4); self.ax.set_ylim(0, 5)
self.ax.set_xlabel("Mach Number M"); self.ax.set_ylabel("Ratio")
self.ax.legend(fontsize=8, framealpha=0.7,
facecolor=C["light_bg"], edgecolor=C["border"])
self.ax.set_title("Rayleigh Flow", fontsize=9)
self._style_ax()
self.canvas.draw()
# ══════════════════════════════════════════════════════════════
# NORMAL SHOCK TAB
# ══════════════════════════════════════════════════════════════
class NormalShockTab(BaseTab):
FLOW_NAME = "Adiabatic Flow With Normal Shock"
GRAPH_TITLE = "Normal Shock Relations vs M₁"
PARAMS = ["M₂","P₂/P₁","T₂/T₁","ρ₂/ρ₁","P₀₂/P₀₁","P₀₂/P₁"]
def _add_radios(self, frame): pass # no radios for shock
def _extra_controls(self, parent, start_row):
ttk.Label(parent, text="(M₁ must be ≥ 1 for a normal shock)",
foreground=C["muted"], font=FONT_SMALL).grid(
row=start_row, column=0, columnspan=2,
sticky="w", padx=8, pady=(0,4))
def _compute(self, M, γ):
if M < 1.0:
messagebox.showwarning("Range Error",
"Normal shock requires M₁ ≥ 1.0")
return None
return normal_shock(M, γ)
def _draw_graph(self, γ):
self.ax.clear()
M1 = np.linspace(1.0, 5, 400)
M2_sq = (M1**2 + 2/(γ-1)) / (2*γ/(γ-1)*M1**2 - 1)
M2 = np.sqrt(np.clip(M2_sq, 0, None))
P2 = (2*γ*M1**2 - (γ-1)) / (γ+1)
T2 = P2 * (2+(γ-1)*M1**2) / ((γ+1)*M1**2)
P02 = (((γ+1)*M1**2/(2+(γ-1)*M1**2))**(γ/(γ-1)) *
((γ+1)/(2*γ*M1**2-(γ-1)))**(1/(γ-1)))
plots = [
("M₂", M2, C["primary"]),
("P₂/P₁", P2, C["forest"]),
("T₂/T₁", T2, "#E07B39"),
("P₀₂/P₀₁", P02, "#5B8DD9"),
]
for lbl, arr, col in plots:
self.ax.plot(M1, arr, color=col, lw=1.8, label=lbl)
self.ax.set_xlim(1, 5); self.ax.set_ylim(0, 10)
self.ax.set_xlabel("Upstream Mach Number M₁")
self.ax.set_ylabel("Ratio / M₂")
self.ax.legend(fontsize=8, framealpha=0.7,
facecolor=C["light_bg"], edgecolor=C["border"])
self.ax.set_title("Normal Shock", fontsize=9)
self._style_ax()
self.canvas.draw()
# ══════════════════════════════════════════════════════════════
if __name__ == "__main__":
import sys
app = pyroCOMPApp()
app.mainloop()
sys.exit(0)'현장과 프로젝트' 카테고리의 다른 글
| ⚖ Unit Converter — 단위 변환기 (0) | 2026.06.11 |
|---|---|
| 재활용이 가능한데 왜 열가소성 추진제를 사용하지 않을까? (0) | 2026.06.05 |
| pygasflow - 준 1차원(Quasi-1D) 이상 기체역학 (0) | 2026.06.03 |
| 상용과 어깨를 나란히 하는 오픈소스 소프트웨어 (0) | 2026.05.29 |
| MATLAB vs Scilab vs GNU Octave: 수치 계산 소프트웨어 비교 (0) | 2026.05.29 |