13. CFD ๊ธฐ์ดˆ (Computational Fluid Dynamics Basics)

13. CFD ๊ธฐ์ดˆ (Computational Fluid Dynamics Basics)

ํ•™์Šต ๋ชฉํ‘œ

  • ์œ ์ฒด์—ญํ•™์˜ ๊ธฐ๋ณธ ์›๋ฆฌ์™€ ์ง€๋ฐฐ๋ฐฉ์ •์‹ ์ดํ•ด
  • ๋ ˆ์ด๋†€์ฆˆ ์ˆ˜์™€ ์œ ๋™ ํŠน์„ฑ ๊ด€๊ณ„ ํŒŒ์•…
  • Navier-Stokes ๋ฐฉ์ •์‹์˜ ์œ ๋„์™€ ์˜๋ฏธ ์ดํ•ด
  • ์••์ถ•์„ฑ/๋น„์••์ถ•์„ฑ ์œ ๋™ ๊ตฌ๋ถ„
  • ๊ฒฝ๊ณ„์ธต ๊ฐœ๋… ํ•™์Šต
  • ๊ฐ„๋‹จํ•œ ์ฑ„๋„ ์œ ๋™ CFD ๊ตฌํ˜„

1. ์œ ์ฒด์—ญํ•™ ๊ธฐ์ดˆ

1.1 ์—ฐ์†์ฒด ๊ฐ€์ •

์—ฐ์†์ฒด ๊ฐ€์ • (Continuum Hypothesis):
- ์œ ์ฒด๋ฅผ ์—ฐ์†์ ์ธ ๋งค์งˆ๋กœ ์ทจ๊ธ‰
- ๊ฐœ๋ณ„ ๋ถ„์ž ๋Œ€์‹  ์œ ์ฒด ์ž…์ž(fluid particle) ๊ฐœ๋… ์‚ฌ์šฉ
- Knudsen ์ˆ˜ Kn = ฮป/L << 1 ์ผ ๋•Œ ์œ ํšจ
  (ฮป: ํ‰๊ท  ์ž์œ  ๊ฒฝ๋กœ, L: ํŠน์„ฑ ๊ธธ์ด)

1.2 ๊ธฐ๋ณธ ๋ฌผ์„ฑ์น˜

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# ์œ ์ฒด ๋ฌผ์„ฑ์น˜ ์˜ˆ์‹œ
class FluidProperties:
    """์œ ์ฒด ๋ฌผ์„ฑ์น˜ ํด๋ž˜์Šค"""

    def __init__(self, name, rho, mu, k=None, cp=None):
        """
        Parameters:
        - name: ์œ ์ฒด ์ด๋ฆ„
        - rho: ๋ฐ€๋„ [kg/mยณ]
        - mu: ๋™์ ์„ฑ๊ณ„์ˆ˜ [Paยทs]
        - k: ์—ด์ „๋„๋„ [W/(mยทK)]
        - cp: ๋น„์—ด [J/(kgยทK)]
        """
        self.name = name
        self.rho = rho      # ๋ฐ€๋„
        self.mu = mu        # ๋™์ ์„ฑ๊ณ„์ˆ˜ (dynamic viscosity)
        self.k = k          # ์—ด์ „๋„๋„
        self.cp = cp        # ์ •์••๋น„์—ด

    @property
    def nu(self):
        """์šด๋™์ ์„ฑ๊ณ„์ˆ˜ (kinematic viscosity)"""
        return self.mu / self.rho

    @property
    def alpha(self):
        """์—ดํ™•์‚ฐ๊ณ„์ˆ˜ (thermal diffusivity)"""
        if self.k and self.cp:
            return self.k / (self.rho * self.cp)
        return None

    @property
    def Pr(self):
        """ํ”„๋ž€ํ‹€ ์ˆ˜ (Prandtl number)"""
        if self.cp:
            return self.mu * self.cp / self.k
        return None

    def __repr__(self):
        return f"FluidProperties({self.name}): rho={self.rho}, mu={self.mu}, nu={self.nu:.2e}"

# ์ผ๋ฐ˜์ ์ธ ์œ ์ฒด๋“ค
water = FluidProperties("Water (20ยฐC)", rho=998, mu=1.002e-3, k=0.598, cp=4182)
air = FluidProperties("Air (20ยฐC)", rho=1.204, mu=1.825e-5, k=0.0257, cp=1007)
oil = FluidProperties("Engine Oil (20ยฐC)", rho=880, mu=0.29, k=0.145, cp=1880)

print(water)
print(f"  Prandtl number: {water.Pr:.2f}")
print(air)
print(f"  Prandtl number: {air.Pr:.2f}")

1.3 ๋ ˆ์ด๋†€์ฆˆ ์ˆ˜ (Reynolds Number)

๋ ˆ์ด๋†€์ฆˆ ์ˆ˜ ์ •์˜:
Re = ฯUL/ฮผ = UL/ฮฝ = (๊ด€์„ฑ๋ ฅ)/(์ ์„ฑ๋ ฅ)

์—ฌ๊ธฐ์„œ:
- ฯ: ์œ ์ฒด ๋ฐ€๋„
- U: ํŠน์„ฑ ์†๋„
- L: ํŠน์„ฑ ๊ธธ์ด
- ฮผ: ๋™์ ์„ฑ๊ณ„์ˆ˜
- ฮฝ = ฮผ/ฯ: ์šด๋™์ ์„ฑ๊ณ„์ˆ˜

์œ ๋™ ํŠน์„ฑ:
- Re < 2300: ์ธต๋ฅ˜ (Laminar flow)
- 2300 < Re < 4000: ์ฒœ์ด ์˜์—ญ (Transition)
- Re > 4000: ๋‚œ๋ฅ˜ (Turbulent flow)
def reynolds_number_analysis():
    """๋ ˆ์ด๋†€์ฆˆ ์ˆ˜์™€ ์œ ๋™ ํŠน์„ฑ ๋ถ„์„"""

    # ๊ด€ ์œ ๋™ ์˜ˆ์ œ
    D = 0.05  # ๊ด€ ์ง๊ฒฝ [m]
    U_range = np.linspace(0.01, 2.0, 100)  # ์œ ์† ๋ฒ”์œ„ [m/s]

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # ๋ฌผ์—์„œ์˜ ๋ ˆ์ด๋†€์ฆˆ ์ˆ˜
    ax1 = axes[0]
    Re_water = water.rho * U_range * D / water.mu

    ax1.plot(U_range, Re_water, 'b-', linewidth=2, label='Water')
    ax1.axhline(y=2300, color='orange', linestyle='--', label='Transition start')
    ax1.axhline(y=4000, color='red', linestyle='--', label='Turbulent')

    ax1.fill_between(U_range, 0, 2300, alpha=0.2, color='green', label='Laminar')
    ax1.fill_between(U_range, 2300, 4000, alpha=0.2, color='orange')
    ax1.fill_between(U_range, 4000, max(Re_water), alpha=0.2, color='red')

    ax1.set_xlabel('Flow Velocity U [m/s]')
    ax1.set_ylabel('Reynolds Number Re')
    ax1.set_title(f'Reynolds Number in Pipe Flow (D = {D*100} cm, Water)')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    ax1.set_yscale('log')

    # ๋‹ค์–‘ํ•œ ์œ ์ฒด ๋น„๊ต
    ax2 = axes[1]
    fluids = [water, air, oil]
    colors = ['blue', 'cyan', 'brown']

    for fluid, color in zip(fluids, colors):
        Re = fluid.rho * U_range * D / fluid.mu
        ax2.plot(U_range, Re, color=color, linewidth=2, label=fluid.name)

    ax2.axhline(y=2300, color='k', linestyle='--', alpha=0.5)
    ax2.set_xlabel('Flow Velocity U [m/s]')
    ax2.set_ylabel('Reynolds Number Re')
    ax2.set_title('Reynolds Number Comparison for Different Fluids')
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    ax2.set_yscale('log')

    plt.tight_layout()
    plt.savefig('reynolds_number.png', dpi=150, bbox_inches='tight')
    plt.show()

    # ์‹ค์šฉ์  ์˜ˆ์ œ ๊ณ„์‚ฐ
    print("\n=== ์‹ค์šฉ์  ๋ ˆ์ด๋†€์ฆˆ ์ˆ˜ ๊ณ„์‚ฐ ์˜ˆ์ œ ===")
    print(f"๊ด€ ์ง๊ฒฝ D = {D*100} cm")

    test_velocities = [0.1, 0.5, 1.0]
    for U in test_velocities:
        Re = water.rho * U * D / water.mu
        regime = "์ธต๋ฅ˜" if Re < 2300 else ("์ฒœ์ด" if Re < 4000 else "๋‚œ๋ฅ˜")
        print(f"  U = {U} m/s -> Re = {Re:.0f} ({regime})")

# reynolds_number_analysis()

2. ์ง€๋ฐฐ๋ฐฉ์ •์‹

2.1 ์—ฐ์† ๋ฐฉ์ •์‹ (Continuity Equation)

์งˆ๋Ÿ‰ ๋ณด์กด ๋ฒ•์น™:
โˆ‚ฯ/โˆ‚t + โˆ‡ยท(ฯu) = 0

ํ…์„œ ํ‘œ๊ธฐ:
โˆ‚ฯ/โˆ‚t + โˆ‚(ฯuแตข)/โˆ‚xแตข = 0

๋น„์••์ถ•์„ฑ ์œ ๋™ (ฯ = const):
โˆ‡ยทu = 0
๋˜๋Š”: โˆ‚u/โˆ‚x + โˆ‚v/โˆ‚y + โˆ‚w/โˆ‚z = 0
def visualize_continuity():
    """์—ฐ์† ๋ฐฉ์ •์‹ ์‹œ๊ฐํ™” - ๊ฒ€์‚ฌ์ฒด์  ์ ‘๊ทผ"""

    fig, axes = plt.subplots(1, 2, figsize=(14, 6))

    # ๊ฒ€์‚ฌ์ฒด์  ๊ฐœ๋…๋„
    ax1 = axes[0]

    # ๊ฒ€์‚ฌ์ฒด์  (์‚ฌ๊ฐํ˜•)
    cv = plt.Rectangle((0.3, 0.3), 0.4, 0.4, fill=False,
                       edgecolor='black', linewidth=2)
    ax1.add_patch(cv)

    # ์งˆ๋Ÿ‰ ์œ ์ž…/์œ ์ถœ ํ™”์‚ดํ‘œ
    # x ๋ฐฉํ–ฅ
    ax1.annotate('', xy=(0.3, 0.5), xytext=(0.1, 0.5),
                arrowprops=dict(arrowstyle='->', color='blue', lw=2))
    ax1.text(0.15, 0.55, r'$\rho u A$', fontsize=12, color='blue')

    ax1.annotate('', xy=(0.9, 0.5), xytext=(0.7, 0.5),
                arrowprops=dict(arrowstyle='->', color='blue', lw=2))
    ax1.text(0.75, 0.55, r'$\rho u A + \frac{\partial(\rho u)}{\partial x}\Delta x A$', fontsize=10, color='blue')

    # y ๋ฐฉํ–ฅ
    ax1.annotate('', xy=(0.5, 0.3), xytext=(0.5, 0.1),
                arrowprops=dict(arrowstyle='->', color='red', lw=2))
    ax1.text(0.55, 0.15, r'$\rho v A$', fontsize=12, color='red')

    ax1.annotate('', xy=(0.5, 0.9), xytext=(0.5, 0.7),
                arrowprops=dict(arrowstyle='->', color='red', lw=2))
    ax1.text(0.55, 0.8, r'$\rho v A + ...$', fontsize=10, color='red')

    ax1.text(0.5, 0.5, r'$\Delta V$', fontsize=14, ha='center', va='center')
    ax1.set_xlim(0, 1)
    ax1.set_ylim(0, 1)
    ax1.set_aspect('equal')
    ax1.set_title('๊ฒ€์‚ฌ์ฒด์ ๊ณผ ์งˆ๋Ÿ‰ ํ”Œ๋Ÿญ์Šค')
    ax1.axis('off')

    # ๋น„์••์ถ•์„ฑ ์œ ๋™์žฅ ์˜ˆ์‹œ (div u = 0)
    ax2 = axes[1]
    x = np.linspace(-2, 2, 20)
    y = np.linspace(-2, 2, 20)
    X, Y = np.meshgrid(x, y)

    # ์œ ๋™์žฅ: u = y, v = -x (์  ์†Œ์Šค ํšŒ์ „ ์œ ๋™, div=0)
    U = Y
    V = -X

    # ๋ฐœ์‚ฐ ๊ณ„์‚ฐ (์ˆ˜์น˜์ )
    div = np.zeros_like(X)

    ax2.streamplot(X, Y, U, V, color='blue', density=1.5, linewidth=1)
    ax2.quiver(X[::2, ::2], Y[::2, ::2], U[::2, ::2], V[::2, ::2],
              color='red', alpha=0.7)
    ax2.set_xlabel('x')
    ax2.set_ylabel('y')
    ax2.set_title(r'๋น„์••์ถ•์„ฑ ์œ ๋™์žฅ ์˜ˆ์‹œ: $u=y, v=-x$ ($\nabla \cdot \mathbf{u} = 0$)')
    ax2.set_aspect('equal')
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('continuity_equation.png', dpi=150, bbox_inches='tight')
    plt.show()

# visualize_continuity()

2.2 ์šด๋™๋Ÿ‰ ๋ฐฉ์ •์‹ (Momentum Equation)

Newton์˜ ์ œ2๋ฒ•์น™ ์ ์šฉ:
ฯ(Du/Dt) = -โˆ‡p + ฮผโˆ‡ยฒu + ฯg + f

์—ฌ๊ธฐ์„œ:
- Du/Dt = โˆ‚u/โˆ‚t + (uยทโˆ‡)u : ๋ฌผ์งˆ ๋ฏธ๋ถ„
- โˆ‡p: ์••๋ ฅ ๊ตฌ๋ฐฐ
- ฮผโˆ‡ยฒu: ์ ์„ฑ๋ ฅ
- ฯg: ์ค‘๋ ฅ
- f: ์™ธ๋ถ€ ์ฒด์ ๋ ฅ

์„ฑ๋ถ„๋ณ„ (๋น„์••์ถ•์„ฑ, 2D):
x: ฯ(โˆ‚u/โˆ‚t + uโˆ‚u/โˆ‚x + vโˆ‚u/โˆ‚y) = -โˆ‚p/โˆ‚x + ฮผ(โˆ‚ยฒu/โˆ‚xยฒ + โˆ‚ยฒu/โˆ‚yยฒ)
y: ฯ(โˆ‚v/โˆ‚t + uโˆ‚v/โˆ‚x + vโˆ‚v/โˆ‚y) = -โˆ‚p/โˆ‚y + ฮผ(โˆ‚ยฒv/โˆ‚xยฒ + โˆ‚ยฒv/โˆ‚yยฒ)

2.3 Navier-Stokes ๋ฐฉ์ •์‹ ์œ ๋„

def navier_stokes_derivation():
    """Navier-Stokes ๋ฐฉ์ •์‹์˜ ๊ฐ ํ•ญ ์˜๋ฏธ ์‹œ๊ฐํ™”"""

    print("=" * 60)
    print("Navier-Stokes ๋ฐฉ์ •์‹ ์œ ๋„")
    print("=" * 60)

    derivation = """
    1. ์งˆ๋Ÿ‰ ๋ณด์กด (์—ฐ์†๋ฐฉ์ •์‹):
       โˆ‚ฯ/โˆ‚t + โˆ‡ยท(ฯu) = 0

       ๋น„์••์ถ•์„ฑ: โˆ‡ยทu = 0

    2. ์šด๋™๋Ÿ‰ ๋ณด์กด (Newton ์ œ2๋ฒ•์น™):

       ๋ฌผ์งˆ ๋ฏธ๋ถ„ (Lagrangian derivative):
       Du/Dt = โˆ‚u/โˆ‚t + (uยทโˆ‡)u
                โ†‘         โ†‘
           ๊ตญ์†Œ๊ฐ€์†๋„  ๋Œ€๋ฅ˜๊ฐ€์†๋„

       ํž˜์˜ ๊ท ํ˜•:
       ฯ(Du/Dt) = โˆ‘F = -โˆ‡p + โˆ‡ยทฯ„ + ฯg
                   โ†‘      โ†‘     โ†‘    โ†‘
                ๊ด€์„ฑ๋ ฅ  ์••๋ ฅ๋ ฅ ์ ์„ฑ๋ ฅ ์ฒด์ ๋ ฅ

       Newton ์œ ์ฒด ๊ฐ€์ • (ฯ„ = ฮผ(โˆ‡u + โˆ‡uแต€)):
       ฯ(Du/Dt) = -โˆ‡p + ฮผโˆ‡ยฒu + ฯg

    3. ๋น„์••์ถ•์„ฑ Navier-Stokes ๋ฐฉ์ •์‹:

       โˆ‚u/โˆ‚t + (uยทโˆ‡)u = -1/ฯ โˆ‡p + ฮฝโˆ‡ยฒu + g

       ์—ฌ๊ธฐ์„œ ฮฝ = ฮผ/ฯ (์šด๋™์ ์„ฑ๊ณ„์ˆ˜)

    4. ๋ฌด์ฐจ์›ํ™” (Dimensionless form):

       ํŠน์„ฑ ์Šค์ผ€์ผ: L(๊ธธ์ด), U(์†๋„), T=L/U(์‹œ๊ฐ„), P=ฯUยฒ(์••๋ ฅ)

       โˆ‚u*/โˆ‚t* + (u*ยทโˆ‡*)u* = -โˆ‡*p* + (1/Re)โˆ‡*ยฒu*

       ์—ฌ๊ธฐ์„œ Re = UL/ฮฝ (๋ ˆ์ด๋†€์ฆˆ ์ˆ˜)
    """
    print(derivation)

    # ๊ฐ ํ•ญ์˜ ์ƒ๋Œ€์  ํฌ๊ธฐ ์‹œ๊ฐํ™”
    fig, ax = plt.subplots(figsize=(12, 6))

    Re_range = np.logspace(0, 6, 100)

    # ๋ฌด์ฐจ์› ํฌ๊ธฐ ๋น„๊ต
    inertia = np.ones_like(Re_range)  # O(1)
    viscous = 1 / Re_range            # O(1/Re)
    pressure = np.ones_like(Re_range) # O(1)

    ax.loglog(Re_range, inertia, 'b-', linewidth=2, label='๊ด€์„ฑํ•ญ O(1)')
    ax.loglog(Re_range, viscous, 'r-', linewidth=2, label='์ ์„ฑํ•ญ O(1/Re)')
    ax.loglog(Re_range, pressure, 'g--', linewidth=2, label='์••๋ ฅํ•ญ O(1)')

    ax.axvline(x=2300, color='gray', linestyle=':', label='์ธต๋ฅ˜-๋‚œ๋ฅ˜ ์ฒœ์ด')
    ax.fill_between(Re_range, 1e-6, 1, where=Re_range < 2300,
                   alpha=0.2, color='green', label='์ ์„ฑ ์ง€๋ฐฐ')
    ax.fill_between(Re_range, 1e-6, 1, where=Re_range >= 2300,
                   alpha=0.2, color='blue', label='๊ด€์„ฑ ์ง€๋ฐฐ')

    ax.set_xlabel('Reynolds Number Re')
    ax.set_ylabel('Relative Magnitude')
    ax.set_title('Navier-Stokes ๋ฐฉ์ •์‹ ๊ฐ ํ•ญ์˜ ์ƒ๋Œ€์  ํฌ๊ธฐ')
    ax.legend()
    ax.grid(True, alpha=0.3)
    ax.set_ylim(1e-6, 10)

    plt.tight_layout()
    plt.savefig('navier_stokes_terms.png', dpi=150, bbox_inches='tight')
    plt.show()

# navier_stokes_derivation()

3. ์••์ถ•์„ฑ vs ๋น„์••์ถ•์„ฑ ์œ ๋™

3.1 ๋งˆํ•˜ ์ˆ˜์™€ ์••์ถ•์„ฑ

๋งˆํ•˜ ์ˆ˜ ์ •์˜:
Ma = U/a

์—ฌ๊ธฐ์„œ:
- U: ์œ ์ฒด ์†๋„
- a: ์Œ์† (์ด์ƒ๊ธฐ์ฒด: a = โˆš(ฮณRT))

๋ถ„๋ฅ˜:
- Ma < 0.3: ๋น„์••์ถ•์„ฑ์œผ๋กœ ์ทจ๊ธ‰ ๊ฐ€๋Šฅ (๋ฐ€๋„ ๋ณ€ํ™” < 5%)
- 0.3 < Ma < 0.8: ์•„์Œ์† (Subsonic)
- 0.8 < Ma < 1.2: ์ฒœ์Œ์† (Transonic)
- 1.2 < Ma < 5: ์ดˆ์Œ์† (Supersonic)
- Ma > 5: ๊ทน์ดˆ์Œ์† (Hypersonic)
def compressibility_analysis():
    """์••์ถ•์„ฑ ํšจ๊ณผ ๋ถ„์„"""

    # ๋“ฑ์—”ํŠธ๋กœํ”ผ ๋ฐ€๋„ ๋น„
    def density_ratio(Ma, gamma=1.4):
        """ฯ/ฯโ‚€ = (1 + (ฮณ-1)/2 Maยฒ)^(-1/(ฮณ-1))"""
        return (1 + (gamma - 1) / 2 * Ma**2) ** (-1 / (gamma - 1))

    # ์••๋ ฅ ๋น„
    def pressure_ratio(Ma, gamma=1.4):
        """p/pโ‚€ = (1 + (ฮณ-1)/2 Maยฒ)^(-ฮณ/(ฮณ-1))"""
        return (1 + (gamma - 1) / 2 * Ma**2) ** (-gamma / (gamma - 1))

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    Ma = np.linspace(0, 3, 200)

    # ๋ฐ€๋„ ๋น„
    ax1 = axes[0]
    rho_ratio = density_ratio(Ma)
    ax1.plot(Ma, rho_ratio, 'b-', linewidth=2)
    ax1.axvline(x=0.3, color='green', linestyle='--', label='Ma=0.3 (๋น„์••์ถ•์„ฑ ํ•œ๊ณ„)')
    ax1.axhline(y=0.95, color='gray', linestyle=':', alpha=0.5)
    ax1.fill_between(Ma, 0, rho_ratio, where=Ma < 0.3, alpha=0.3, color='green',
                    label='๋น„์••์ถ•์„ฑ ์˜์—ญ')

    ax1.set_xlabel('Mach Number Ma')
    ax1.set_ylabel(r'Density Ratio $\rho/\rho_0$')
    ax1.set_title('๋“ฑ์—”ํŠธ๋กœํ”ผ ์œ ๋™์—์„œ์˜ ๋ฐ€๋„ ๋ณ€ํ™”')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    ax1.set_xlim(0, 3)
    ax1.set_ylim(0, 1.1)

    # ์••์ถ•์„ฑ/๋น„์••์ถ•์„ฑ ๋ฐฉ์ •์‹ ๋น„๊ต
    ax2 = axes[1]

    equations = {
        '๋น„์••์ถ•์„ฑ\n(Incompressible)': [
            r'$\nabla \cdot \mathbf{u} = 0$',
            r'$\rho \frac{D\mathbf{u}}{Dt} = -\nabla p + \mu \nabla^2 \mathbf{u}$',
            '3๊ฐœ ๋ฐฉ์ •์‹, 4๊ฐœ ๋ฏธ์ง€์ˆ˜ (u,v,w,p)',
            '์••๋ ฅ: Poisson ๋ฐฉ์ •์‹์œผ๋กœ ๊ฒฐ์ •'
        ],
        '์••์ถ•์„ฑ\n(Compressible)': [
            r'$\frac{\partial \rho}{\partial t} + \nabla \cdot (\rho \mathbf{u}) = 0$',
            r'$\frac{\partial (\rho \mathbf{u})}{\partial t} + \nabla \cdot (\rho \mathbf{u} \otimes \mathbf{u}) = -\nabla p + \nabla \cdot \tau$',
            r'$\frac{\partial E}{\partial t} + \nabla \cdot ((E+p)\mathbf{u}) = \nabla \cdot (k\nabla T + \tau \cdot \mathbf{u})$',
            '5๊ฐœ ๋ฐฉ์ •์‹, 5๊ฐœ ๋ฏธ์ง€์ˆ˜ (ฯ,u,v,w,E) + ์ƒํƒœ๋ฐฉ์ •์‹'
        ]
    }

    ax2.text(0.25, 0.95, '๋น„์••์ถ•์„ฑ (Ma < 0.3)', fontsize=14, fontweight='bold',
            ha='center', transform=ax2.transAxes)
    ax2.text(0.75, 0.95, '์••์ถ•์„ฑ (Ma > 0.3)', fontsize=14, fontweight='bold',
            ha='center', transform=ax2.transAxes)

    y_pos = 0.85
    for eq in equations['๋น„์••์ถ•์„ฑ\n(Incompressible)']:
        ax2.text(0.25, y_pos, eq, fontsize=10, ha='center', transform=ax2.transAxes)
        y_pos -= 0.12

    y_pos = 0.85
    for eq in equations['์••์ถ•์„ฑ\n(Compressible)']:
        ax2.text(0.75, y_pos, eq, fontsize=10, ha='center', transform=ax2.transAxes)
        y_pos -= 0.12

    ax2.axvline(x=0.5, color='black', linestyle='-', linewidth=2,
               transform=ax2.transAxes)
    ax2.axis('off')
    ax2.set_title('์ง€๋ฐฐ๋ฐฉ์ •์‹ ๋น„๊ต')

    plt.tight_layout()
    plt.savefig('compressibility.png', dpi=150, bbox_inches='tight')
    plt.show()

# compressibility_analysis()

4. ๊ฒฝ๊ณ„์ธต ์ด๋ก 

4.1 ๊ฒฝ๊ณ„์ธต ๊ฐœ๋…

๊ฒฝ๊ณ„์ธต (Boundary Layer):
- ๋ฒฝ๋ฉด ๊ทผ์ฒ˜์—์„œ ์ ์„ฑ ํšจ๊ณผ๊ฐ€ ์ง€๋ฐฐ์ ์ธ ์˜์—ญ
- ๋ฒฝ๋ฉด: no-slip ์กฐ๊ฑด (u = 0)
- ๊ฒฝ๊ณ„์ธต ์™ธ๋ถ€: ์ž์œ ๋ฅ˜ ์†๋„ Uโˆž

๊ฒฝ๊ณ„์ธต ๋‘๊ป˜ ฮด:
- ์†๋„๊ฐ€ ์ž์œ ๋ฅ˜์˜ 99%๊ฐ€ ๋˜๋Š” ์œ„์น˜
- ์ธต๋ฅ˜ ํ‰ํŒ: ฮด ~ x/โˆšRe_x (Blasius)
- ๋‚œ๋ฅ˜ ํ‰ํŒ: ฮด ~ x/Re_x^(1/5)
def boundary_layer_theory():
    """๊ฒฝ๊ณ„์ธต ์ด๋ก  ์‹œ๊ฐํ™”"""

    fig, axes = plt.subplots(2, 2, figsize=(14, 10))

    # (1) Blasius ์ธต๋ฅ˜ ๊ฒฝ๊ณ„์ธต ํ”„๋กœํŒŒ์ผ
    ax1 = axes[0, 0]

    # Blasius ์œ ์‚ฌํ•ด (๊ทผ์‚ฌ)
    eta = np.linspace(0, 8, 100)  # ๋ฌด์ฐจ์› ์ขŒํ‘œ ฮท = yโˆš(Uโˆž/ฮฝx)

    # f'(ฮท) ๊ทผ์‚ฌ (์‹ค์ œ๋Š” Blasius ๋ฐฉ์ •์‹ ์ˆ˜์น˜ํ•ด)
    # ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํ•œ ๊ทผ์‚ฌ ์‚ฌ์šฉ
    u_U = np.tanh(eta / 2.5) ** 1.5  # ๊ทผ์‚ฌ

    ax1.plot(u_U, eta, 'b-', linewidth=2)
    ax1.axhline(y=5.0, color='red', linestyle='--', label=r'$\delta_{99}$ (ฮท โ‰ˆ 5)')
    ax1.fill_betweenx(eta, 0, u_U, alpha=0.3)

    ax1.set_xlabel(r'$u/U_\infty$')
    ax1.set_ylabel(r'$\eta = y\sqrt{U_\infty/\nu x}$')
    ax1.set_title('Blasius ์ธต๋ฅ˜ ๊ฒฝ๊ณ„์ธต ์†๋„ ํ”„๋กœํŒŒ์ผ')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    ax1.set_xlim(0, 1.1)
    ax1.set_ylim(0, 8)

    # (2) ๊ฒฝ๊ณ„์ธต ๋‘๊ป˜ ์„ฑ์žฅ
    ax2 = axes[0, 1]

    nu = 1.5e-5  # ๊ณต๊ธฐ ์šด๋™์ ์„ฑ๊ณ„์ˆ˜
    U_inf = 10   # ์ž์œ ๋ฅ˜ ์†๋„ [m/s]
    x = np.linspace(0.01, 1, 100)  # ํ‰ํŒ ์œ„์น˜ [m]

    Re_x = U_inf * x / nu

    # ์ธต๋ฅ˜ ๊ฒฝ๊ณ„์ธต ๋‘๊ป˜ (Blasius)
    delta_lam = 5.0 * x / np.sqrt(Re_x)

    # ๋‚œ๋ฅ˜ ๊ฒฝ๊ณ„์ธต ๋‘๊ป˜ (1/7์Šน ๋ฒ•์น™)
    delta_turb = 0.37 * x / Re_x ** 0.2

    # ์ฒœ์ด ์œ„์น˜ (Re_x ~ 5ร—10^5)
    x_trans = 5e5 * nu / U_inf

    ax2.plot(x * 1000, delta_lam * 1000, 'b-', linewidth=2, label='์ธต๋ฅ˜')
    ax2.plot(x * 1000, delta_turb * 1000, 'r-', linewidth=2, label='๋‚œ๋ฅ˜')
    ax2.axvline(x=x_trans * 1000, color='green', linestyle='--', label=f'์ฒœ์ด์  (x โ‰ˆ {x_trans*1000:.0f} mm)')

    ax2.set_xlabel('x [mm]')
    ax2.set_ylabel(r'$\delta$ [mm]')
    ax2.set_title(f'๊ฒฝ๊ณ„์ธต ๋‘๊ป˜ ์„ฑ์žฅ (Uโˆž = {U_inf} m/s, ๊ณต๊ธฐ)')
    ax2.legend()
    ax2.grid(True, alpha=0.3)

    # (3) ๋ฒฝ๋ฉด ์ „๋‹จ์‘๋ ฅ
    ax3 = axes[1, 0]

    # ์ธต๋ฅ˜ ๋ฒฝ๋ฉด ์ „๋‹จ์‘๋ ฅ ๊ณ„์ˆ˜
    Cf_lam = 0.664 / np.sqrt(Re_x)

    # ๋‚œ๋ฅ˜ ๋ฒฝ๋ฉด ์ „๋‹จ์‘๋ ฅ ๊ณ„์ˆ˜
    Cf_turb = 0.027 / Re_x ** (1/7)

    ax3.loglog(Re_x, Cf_lam, 'b-', linewidth=2, label='์ธต๋ฅ˜ (Blasius)')
    ax3.loglog(Re_x, Cf_turb, 'r-', linewidth=2, label='๋‚œ๋ฅ˜ (1/7์Šน ๋ฒ•์น™)')
    ax3.axvline(x=5e5, color='green', linestyle='--', alpha=0.5)

    ax3.set_xlabel(r'$Re_x$')
    ax3.set_ylabel(r'$C_f = \tau_w / (0.5\rho U_\infty^2)$')
    ax3.set_title('๋ฒฝ๋ฉด ๋งˆ์ฐฐ ๊ณ„์ˆ˜')
    ax3.legend()
    ax3.grid(True, alpha=0.3)

    # (4) ๊ฒฝ๊ณ„์ธต ๊ฐœ๋…๋„
    ax4 = axes[1, 1]

    # ํ‰ํŒ
    ax4.fill_between([0, 5], [-0.1, -0.1], [0, 0], color='gray', alpha=0.5)

    # ๊ฒฝ๊ณ„์ธต
    x_plate = np.linspace(0, 5, 50)
    delta_vis = 0.5 * np.sqrt(x_plate)  # ๋‹จ์ˆœํ™”๋œ ๊ฒฝ๊ณ„์ธต
    ax4.fill_between(x_plate, 0, delta_vis, alpha=0.3, color='blue', label='๊ฒฝ๊ณ„์ธต')
    ax4.plot(x_plate, delta_vis, 'b-', linewidth=2)

    # ์†๋„ ํ”„๋กœํŒŒ์ผ ํ™”์‚ดํ‘œ
    for x0 in [0.5, 1.5, 3.0, 4.5]:
        y_arrows = np.linspace(0, 0.5 * np.sqrt(x0) * 1.2, 6)
        for y in y_arrows:
            u = min(1, y / (0.5 * np.sqrt(x0))) if x0 > 0 else 0
            ax4.annotate('', xy=(x0 + u * 0.3, y), xytext=(x0, y),
                        arrowprops=dict(arrowstyle='->', color='red', lw=0.8))

    # ์ž์œ ๋ฅ˜
    ax4.annotate('', xy=(5, 1.5), xytext=(0, 1.5),
                arrowprops=dict(arrowstyle='->', color='black', lw=2))
    ax4.text(2.5, 1.7, r'$U_\infty$', fontsize=14)

    ax4.text(2.5, 0.3, '๊ฒฝ๊ณ„์ธต', fontsize=12, color='blue')
    ax4.set_xlabel('x')
    ax4.set_ylabel('y')
    ax4.set_title('ํ‰ํŒ ์œ„ ๊ฒฝ๊ณ„์ธต ๋ฐœ๋‹ฌ')
    ax4.set_xlim(-0.5, 5.5)
    ax4.set_ylim(-0.2, 2)
    ax4.set_aspect('equal')
    ax4.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('boundary_layer.png', dpi=150, bbox_inches='tight')
    plt.show()

# boundary_layer_theory()

5. ๊ฐ„๋‹จํ•œ CFD ์˜ˆ์ œ: Poiseuille ์œ ๋™

5.1 2D ์ฑ„๋„ ์œ ๋™ (Poiseuille Flow)

๋ฌธ์ œ ์„ค์ •:
- ๋‘ ํ‰ํ–‰ ํ‰ํŒ ์‚ฌ์ด์˜ ์ •์ƒ ์ธต๋ฅ˜ ์œ ๋™
- ์••๋ ฅ ๊ตฌ๋ฐฐ์— ์˜ํ•ด ๊ตฌ๋™
- ํ•ด์„ํ•ด ์กด์žฌ (๊ฒ€์ฆ์šฉ)

์ง€๋ฐฐ๋ฐฉ์ •์‹ (์ •์ƒ, ์™„์ „๋ฐœ๋‹ฌ):
dยฒu/dyยฒ = (1/ฮผ)(dp/dx) = const

๊ฒฝ๊ณ„์กฐ๊ฑด:
- y = 0: u = 0 (no-slip)
- y = H: u = 0 (no-slip)

ํ•ด์„ํ•ด:
u(y) = -(1/2ฮผ)(dp/dx)y(H-y)

์ตœ๋Œ€ ์†๋„ (์ค‘์‹ฌ):
u_max = (Hยฒ/8ฮผ)|dp/dx|
def poiseuille_flow_exact():
    """Poiseuille ์œ ๋™ ํ•ด์„ํ•ด"""

    H = 1.0       # ์ฑ„๋„ ๋†’์ด [m]
    mu = 0.01     # ๋™์ ์„ฑ๊ณ„์ˆ˜ [Paยทs]
    dpdx = -1.0   # ์••๋ ฅ ๊ตฌ๋ฐฐ [Pa/m] (์Œ์ˆ˜ = ์–‘์˜ x ๋ฐฉํ–ฅ ์œ ๋™)

    y = np.linspace(0, H, 100)
    u_exact = -(1 / (2 * mu)) * dpdx * y * (H - y)

    u_max = H**2 / (8 * mu) * abs(dpdx)
    u_avg = 2 / 3 * u_max

    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # ์†๋„ ํ”„๋กœํŒŒ์ผ
    ax1 = axes[0]
    ax1.plot(u_exact, y, 'b-', linewidth=2, label='ํ•ด์„ํ•ด')
    ax1.axhline(y=H/2, color='red', linestyle='--', alpha=0.5)
    ax1.axvline(x=u_max, color='green', linestyle='--', alpha=0.5, label=f'u_max = {u_max:.2f}')
    ax1.axvline(x=u_avg, color='orange', linestyle='--', alpha=0.5, label=f'u_avg = {u_avg:.2f}')

    ax1.set_xlabel('u [m/s]')
    ax1.set_ylabel('y [m]')
    ax1.set_title('Poiseuille ์œ ๋™ ์†๋„ ํ”„๋กœํŒŒ์ผ')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    # ์ „๋‹จ์‘๋ ฅ ํ”„๋กœํŒŒ์ผ
    ax2 = axes[1]
    tau = mu * np.gradient(u_exact, y)
    ax2.plot(tau, y, 'r-', linewidth=2)
    ax2.axvline(x=0, color='black', linestyle='-', alpha=0.3)

    ax2.set_xlabel(r'$\tau$ [Pa]')
    ax2.set_ylabel('y [m]')
    ax2.set_title('์ „๋‹จ์‘๋ ฅ ๋ถ„ํฌ')
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('poiseuille_exact.png', dpi=150, bbox_inches='tight')
    plt.show()

    return u_exact, y

# u_exact, y = poiseuille_flow_exact()

5.2 ์œ ํ•œ์ฐจ๋ถ„๋ฒ•์„ ์ด์šฉํ•œ CFD ๊ตฌํ˜„

def cfd_channel_flow():
    """
    2D ์ฑ„๋„ ์œ ๋™ CFD ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    - ๋น„์ •์ƒ Navier-Stokes ๋ฐฉ์ •์‹
    - ์ •์ƒ ์ƒํƒœ๊นŒ์ง€ ์‹œ๊ฐ„ ์ „์ง„
    """

    # ๊ฒฉ์ž ์„ค์ •
    Nx = 50       # x ๋ฐฉํ–ฅ ๊ฒฉ์ž ์ˆ˜
    Ny = 30       # y ๋ฐฉํ–ฅ ๊ฒฉ์ž ์ˆ˜
    Lx = 2.0      # ์ฑ„๋„ ๊ธธ์ด [m]
    Ly = 1.0      # ์ฑ„๋„ ๋†’์ด [m]

    dx = Lx / (Nx - 1)
    dy = Ly / (Ny - 1)

    x = np.linspace(0, Lx, Nx)
    y = np.linspace(0, Ly, Ny)
    X, Y = np.meshgrid(x, y)

    # ๋ฌผ์„ฑ์น˜
    rho = 1.0     # ๋ฐ€๋„ [kg/mยณ]
    mu = 0.01     # ๋™์ ์„ฑ๊ณ„์ˆ˜ [Paยทs]
    nu = mu / rho

    # ์••๋ ฅ ๊ตฌ๋ฐฐ (์ฒด์ ๋ ฅ์œผ๋กœ ๋ชจ๋ธ๋ง)
    dpdx = -1.0   # [Pa/m]

    # ์‹œ๊ฐ„ ์„ค์ •
    dt = 0.001
    n_steps = 2000

    # ์ดˆ๊ธฐํ™”
    u = np.zeros((Ny, Nx))  # x-์†๋„
    v = np.zeros((Ny, Nx))  # y-์†๋„
    p = np.zeros((Ny, Nx))  # ์••๋ ฅ

    # CFL ์กฐ๊ฑด ํ™•์ธ
    u_max_expected = Ly**2 / (8 * mu) * abs(dpdx)
    CFL = u_max_expected * dt / dx
    print(f"์˜ˆ์ƒ ์ตœ๋Œ€ ์†๋„: {u_max_expected:.4f} m/s")
    print(f"CFL ์ˆ˜: {CFL:.4f}")

    # ํ•ด์„ํ•ด (๊ฒ€์ฆ์šฉ)
    u_exact = -(1 / (2 * mu)) * dpdx * y * (Ly - y)

    def apply_boundary_conditions(u, v):
        """๊ฒฝ๊ณ„์กฐ๊ฑด ์ ์šฉ"""
        # ๋ฒฝ๋ฉด (no-slip)
        u[0, :] = 0    # ํ•˜๋‹จ ๋ฒฝ
        u[-1, :] = 0   # ์ƒ๋‹จ ๋ฒฝ
        v[0, :] = 0
        v[-1, :] = 0

        # ์ž…๊ตฌ/์ถœ๊ตฌ (์ฃผ๊ธฐ์  ๋˜๋Š” Neumann)
        u[:, 0] = u[:, 1]    # ์ž…๊ตฌ
        u[:, -1] = u[:, -2]  # ์ถœ๊ตฌ
        v[:, 0] = 0
        v[:, -1] = v[:, -2]

        return u, v

    def compute_rhs(u, v, p, nu, dx, dy, dpdx, rho):
        """์šฐ๋ณ€ ๊ณ„์‚ฐ (์šด๋™๋Ÿ‰ ๋ฐฉ์ •์‹)"""
        Ny, Nx = u.shape
        rhs_u = np.zeros_like(u)
        rhs_v = np.zeros_like(v)

        for i in range(1, Ny-1):
            for j in range(1, Nx-1):
                # ๋Œ€๋ฅ˜ํ•ญ (์ค‘์‹ฌ์ฐจ๋ถ„)
                duudx = (u[i, j+1]**2 - u[i, j-1]**2) / (2 * dx)
                duvdy = (u[i+1, j] * v[i+1, j] - u[i-1, j] * v[i-1, j]) / (2 * dy)

                dvudx = (v[i, j+1] * u[i, j+1] - v[i, j-1] * u[i, j-1]) / (2 * dx)
                dvvdy = (v[i+1, j]**2 - v[i-1, j]**2) / (2 * dy)

                # ํ™•์‚ฐํ•ญ (์ค‘์‹ฌ์ฐจ๋ถ„)
                d2udx2 = (u[i, j+1] - 2*u[i, j] + u[i, j-1]) / dx**2
                d2udy2 = (u[i+1, j] - 2*u[i, j] + u[i-1, j]) / dy**2

                d2vdx2 = (v[i, j+1] - 2*v[i, j] + v[i, j-1]) / dx**2
                d2vdy2 = (v[i+1, j] - 2*v[i, j] + v[i-1, j]) / dy**2

                # ์••๋ ฅํ•ญ
                dpdx_local = (p[i, j+1] - p[i, j-1]) / (2 * dx) if j > 0 and j < Nx-1 else 0
                dpdy_local = (p[i+1, j] - p[i-1, j]) / (2 * dy) if i > 0 and i < Ny-1 else 0

                # ์šด๋™๋Ÿ‰ ๋ฐฉ์ •์‹ ์šฐ๋ณ€
                rhs_u[i, j] = -duudx - duvdy - dpdx_local/rho + nu * (d2udx2 + d2udy2) - dpdx/rho
                rhs_v[i, j] = -dvudx - dvvdy - dpdy_local/rho + nu * (d2vdx2 + d2vdy2)

        return rhs_u, rhs_v

    # ์‹œ๊ฐ„ ์ „์ง„
    history = []

    for n in range(n_steps):
        # ๊ฒฝ๊ณ„์กฐ๊ฑด
        u, v = apply_boundary_conditions(u, v)

        # RHS ๊ณ„์‚ฐ
        rhs_u, rhs_v = compute_rhs(u, v, p, nu, dx, dy, dpdx, rho)

        # ์‹œ๊ฐ„ ์ „์ง„ (Euler)
        u = u + dt * rhs_u
        v = v + dt * rhs_v

        # ์ˆ˜๋ ด ์ฒดํฌ
        if n % 200 == 0:
            u_center = u[:, Nx//2]
            error = np.max(np.abs(u_center - u_exact))
            history.append((n, error, np.max(u)))
            print(f"Step {n}: max error = {error:.6f}, max u = {np.max(u):.6f}")

    # ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))

    # (1) ์†๋„์žฅ (๋ฒกํ„ฐ)
    ax1 = axes[0, 0]
    skip = 2
    ax1.quiver(X[::skip, ::skip], Y[::skip, ::skip],
              u[::skip, ::skip], v[::skip, ::skip],
              color='blue', scale=30)
    ax1.set_xlabel('x [m]')
    ax1.set_ylabel('y [m]')
    ax1.set_title('์†๋„ ๋ฒกํ„ฐ์žฅ')
    ax1.set_aspect('equal')

    # (2) u-์†๋„ ๋“ฑ๊ณ ์„ 
    ax2 = axes[0, 1]
    cf = ax2.contourf(X, Y, u, levels=20, cmap='jet')
    plt.colorbar(cf, ax=ax2, label='u [m/s]')
    ax2.set_xlabel('x [m]')
    ax2.set_ylabel('y [m]')
    ax2.set_title('u-์†๋„ ๋ถ„ํฌ')
    ax2.set_aspect('equal')

    # (3) ํ•ด์„ํ•ด์™€ ๋น„๊ต
    ax3 = axes[1, 0]
    u_center = u[:, Nx//2]
    ax3.plot(u_center, y, 'bo-', markersize=4, label='CFD')
    ax3.plot(u_exact, y, 'r-', linewidth=2, label='ํ•ด์„ํ•ด')
    ax3.set_xlabel('u [m/s]')
    ax3.set_ylabel('y [m]')
    ax3.set_title('์†๋„ ํ”„๋กœํŒŒ์ผ ๋น„๊ต (x = L/2)')
    ax3.legend()
    ax3.grid(True, alpha=0.3)

    # (4) ์ˆ˜๋ ด ์ด๋ ฅ
    ax4 = axes[1, 1]
    steps, errors, u_maxs = zip(*history)
    ax4.semilogy(steps, errors, 'b-o', label='Error')
    ax4.set_xlabel('Time Step')
    ax4.set_ylabel('Max Error')
    ax4.set_title('์ˆ˜๋ ด ์ด๋ ฅ')
    ax4.legend()
    ax4.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('cfd_channel_flow.png', dpi=150, bbox_inches='tight')
    plt.show()

    return u, v, p, X, Y

# u, v, p, X, Y = cfd_channel_flow()

6. CFD์˜ ์ฃผ์š” ๊ณผ์ œ

6.1 ๊ฒฉ์ž ์ƒ์„ฑ (Mesh Generation)

def mesh_types_visualization():
    """CFD ๊ฒฉ์ž ์œ ํ˜• ์‹œ๊ฐํ™”"""

    fig, axes = plt.subplots(2, 2, figsize=(12, 10))

    # (1) ๊ตฌ์กฐ ๊ฒฉ์ž (Structured)
    ax1 = axes[0, 0]
    nx, ny = 10, 8
    x = np.linspace(0, 2, nx)
    y = np.linspace(0, 1, ny)

    for i in range(ny):
        ax1.plot(x, np.full_like(x, y[i]), 'b-', linewidth=0.5)
    for j in range(nx):
        ax1.plot(np.full_like(y, x[j]), y, 'b-', linewidth=0.5)

    X, Y = np.meshgrid(x, y)
    ax1.plot(X, Y, 'ko', markersize=3)
    ax1.set_title('๊ตฌ์กฐ ๊ฒฉ์ž (Structured)')
    ax1.set_aspect('equal')
    ax1.set_xlim(-0.1, 2.1)
    ax1.set_ylim(-0.1, 1.1)

    # (2) ๋น„๊ตฌ์กฐ ๊ฒฉ์ž (Unstructured)
    ax2 = axes[0, 1]
    from scipy.spatial import Delaunay

    # ๋žœ๋ค ํฌ์ธํŠธ
    np.random.seed(42)
    points = np.random.rand(30, 2)
    points[:, 0] *= 2

    # ๊ฒฝ๊ณ„ ํฌ์ธํŠธ ์ถ”๊ฐ€
    boundary = np.array([[0, 0], [2, 0], [2, 1], [0, 1]])
    for i in range(4):
        edge = np.linspace(boundary[i], boundary[(i+1)%4], 8)[1:-1]
        points = np.vstack([points, edge])

    tri = Delaunay(points)
    ax2.triplot(points[:, 0], points[:, 1], tri.simplices, 'b-', linewidth=0.5)
    ax2.plot(points[:, 0], points[:, 1], 'ko', markersize=3)
    ax2.set_title('๋น„๊ตฌ์กฐ ๊ฒฉ์ž (Unstructured)')
    ax2.set_aspect('equal')
    ax2.set_xlim(-0.1, 2.1)
    ax2.set_ylim(-0.1, 1.1)

    # (3) O-๊ฒฉ์ž (์›ํ†ต ์ฃผ์œ„)
    ax3 = axes[1, 0]
    r_inner = 0.3
    r_outer = 1.0
    n_r = 8
    n_theta = 24

    r = np.linspace(r_inner, r_outer, n_r)
    theta = np.linspace(0, 2*np.pi, n_theta)

    for ri in r:
        x_circle = ri * np.cos(theta)
        y_circle = ri * np.sin(theta)
        ax3.plot(x_circle, y_circle, 'b-', linewidth=0.5)

    for ti in theta[:-1]:
        x_radial = r * np.cos(ti)
        y_radial = r * np.sin(ti)
        ax3.plot(x_radial, y_radial, 'b-', linewidth=0.5)

    R, Theta = np.meshgrid(r, theta)
    X_o = R * np.cos(Theta)
    Y_o = R * np.sin(Theta)
    ax3.plot(X_o, Y_o, 'ko', markersize=2)

    ax3.set_title('O-๊ฒฉ์ž (์›ํ†ต ์ฃผ์œ„)')
    ax3.set_aspect('equal')
    ax3.set_xlim(-1.2, 1.2)
    ax3.set_ylim(-1.2, 1.2)

    # (4) ๊ฒฝ๊ณ„์ธต ํ”„๋ฆฌ์ฆ˜ ๊ฒฉ์ž
    ax4 = axes[1, 1]

    # ๊ฒฝ๊ณ„์ธต ์˜์—ญ (ํ”„๋ฆฌ์ฆ˜)
    x_wall = np.linspace(0, 2, 20)
    y_layers = [0, 0.02, 0.05, 0.1, 0.2, 0.4]

    for yl in y_layers:
        ax4.plot(x_wall, np.full_like(x_wall, yl), 'b-', linewidth=0.5)

    # ์™ธ๋ถ€ ๋น„๊ตฌ์กฐ ๊ฒฉ์ž (์‚ผ๊ฐํ˜•)
    np.random.seed(123)
    outer_points = np.random.rand(20, 2)
    outer_points[:, 0] *= 2
    outer_points[:, 1] = outer_points[:, 1] * 0.5 + 0.4

    tri_outer = Delaunay(outer_points)
    ax4.triplot(outer_points[:, 0], outer_points[:, 1], tri_outer.simplices,
               'g-', linewidth=0.5)

    ax4.fill_between(x_wall, 0, y_layers[-1], alpha=0.2, color='blue',
                    label='๊ฒฝ๊ณ„์ธต (ํ”„๋ฆฌ์ฆ˜)')
    ax4.set_title('ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒฉ์ž (ํ”„๋ฆฌ์ฆ˜ + ์‚ผ๊ฐํ˜•)')
    ax4.set_aspect('equal')
    ax4.set_xlim(-0.1, 2.1)
    ax4.set_ylim(-0.05, 1)
    ax4.legend()

    plt.tight_layout()
    plt.savefig('mesh_types.png', dpi=150, bbox_inches='tight')
    plt.show()

# mesh_types_visualization()

6.2 ๋‚œ๋ฅ˜ ๋ชจ๋ธ๋ง

์ฃผ์š” ๋‚œ๋ฅ˜ ๋ชจ๋ธ:

1. RANS (Reynolds-Averaged Navier-Stokes):
   - k-ฮต ๋ชจ๋ธ: ๋ฒ”์šฉ, ์‚ฐ์—… ํ‘œ์ค€
   - k-ฯ‰ ๋ชจ๋ธ: ๋ฒฝ๋ฉด ๊ทผ์ฒ˜ ์ •ํ™•
   - SST ๋ชจ๋ธ: k-ฮต + k-ฯ‰ ์žฅ์  ๊ฒฐํ•ฉ

2. LES (Large Eddy Simulation):
   - ํฐ ์™€๋ฅ˜๋Š” ์ง์ ‘ ๊ณ„์‚ฐ
   - ์ž‘์€ ์™€๋ฅ˜๋Š” ๋ชจ๋ธ๋ง (SGS ๋ชจ๋ธ)

3. DNS (Direct Numerical Simulation):
   - ๋ชจ๋“  ๋‚œ๋ฅ˜ ์Šค์ผ€์ผ ์ง์ ‘ ๊ณ„์‚ฐ
   - Reยณ์— ๋น„๋ก€ํ•˜๋Š” ๊ณ„์‚ฐ ๋น„์šฉ

์„ ํƒ ๊ธฐ์ค€:
- ์ •ํ™•๋„: DNS > LES > RANS
- ๊ณ„์‚ฐ ๋น„์šฉ: DNS > LES > RANS
- ์‹ค์šฉ์„ฑ: RANS > LES > DNS

7. ์—ฐ์Šต ๋ฌธ์ œ

์—ฐ์Šต 1: ๋ ˆ์ด๋†€์ฆˆ ์ˆ˜ ๊ณ„์‚ฐ

์ง๊ฒฝ 5cm ๊ด€์—์„œ ๋ฌผ(20ยฐC)์ด ํ‰๊ท  ์†๋„ 2m/s๋กœ ํ๋ฅผ ๋•Œ ๋ ˆ์ด๋†€์ฆˆ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ์œ ๋™ ์ƒํƒœ๋ฅผ ํŒ๋ณ„ํ•˜์‹œ์˜ค.

์—ฐ์Šต 2: Poiseuille ์œ ๋™

Poiseuille ์œ ๋™์—์„œ ํ‰๊ท  ์†๋„์™€ ์ตœ๋Œ€ ์†๋„์˜ ๊ด€๊ณ„๋ฅผ ์œ ๋„ํ•˜์‹œ์˜ค.

์—ฐ์Šต 3: ๊ฒฝ๊ณ„์ธต ๋‘๊ป˜

๊ณต๊ธฐ(20ยฐC)๊ฐ€ 5m/s๋กœ ํ‰ํŒ ์œ„๋ฅผ ํ๋ฅผ ๋•Œ, ์„ ๋‹จ์—์„œ 10cm ๋–จ์–ด์ง„ ์œ„์น˜์˜ ์ธต๋ฅ˜ ๊ฒฝ๊ณ„์ธต ๋‘๊ป˜๋ฅผ ๊ณ„์‚ฐํ•˜์‹œ์˜ค.

์—ฐ์Šต 4: CFD ๊ฒฉ์ž ์˜์กด์„ฑ

์ฑ„๋„ ์œ ๋™ CFD ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ ๊ฒฉ์ž ์ˆ˜๋ฅผ ๋ณ€ํ™”์‹œํ‚ค๋ฉด์„œ ๊ฒฉ์ž ์ˆ˜๋ ด ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜์‹œ์˜ค.


8. ์ฐธ๊ณ ์ž๋ฃŒ

ํ•ต์‹ฌ ๊ต์žฌ

  • Versteeg & Malalasekera, "An Introduction to Computational Fluid Dynamics"
  • Anderson, "Computational Fluid Dynamics: The Basics with Applications"
  • Ferziger & Peric, "Computational Methods for Fluid Dynamics"

CFD ์†Œํ”„ํŠธ์›จ์–ด

  • OpenFOAM (์˜คํ”ˆ์†Œ์Šค)
  • ANSYS Fluent (์ƒ์šฉ)
  • COMSOL Multiphysics (์ƒ์šฉ)
  • SU2 (์˜คํ”ˆ์†Œ์Šค)

์˜จ๋ผ์ธ ์ž๋ฃŒ

  • NASA CFD Resources
  • CFD Online (ํฌ๋Ÿผ, ํŠœํ† ๋ฆฌ์–ผ)
  • LearnCAx (๋ฌด๋ฃŒ ๊ฐ•์ขŒ)

์š”์•ฝ

CFD ๊ธฐ์ดˆ ํ•ต์‹ฌ:

1. ์ง€๋ฐฐ๋ฐฉ์ •์‹:
   - ์—ฐ์†: โˆ‡ยทu = 0 (๋น„์••์ถ•์„ฑ)
   - ์šด๋™๋Ÿ‰: ฯ(Du/Dt) = -โˆ‡p + ฮผโˆ‡ยฒu
   - ์—๋„ˆ์ง€: (์••์ถ•์„ฑ ์œ ๋™)

2. ๋ฌด์ฐจ์› ์ˆ˜:
   - Re = ฯUL/ฮผ (๊ด€์„ฑ/์ ์„ฑ)
   - Ma = U/a (์••์ถ•์„ฑ)
   - Pr = ฮฝ/ฮฑ (์šด๋™๋Ÿ‰/์—ด ํ™•์‚ฐ)

3. CFD ์ ˆ์ฐจ:
   โ‘  ๊ฒฉ์ž ์ƒ์„ฑ (์ „์ฒ˜๋ฆฌ)
   โ‘ก ์ด์‚ฐํ™” ๋ฐ ๊ณ„์‚ฐ (์†”๋ฒ„)
   โ‘ข ํ›„์ฒ˜๋ฆฌ (์‹œ๊ฐํ™”, ๋ถ„์„)

4. ์ฃผ์š” ๊ณ ๋ ค์‚ฌํ•ญ:
   - ๊ฒฉ์ž ํ’ˆ์งˆ ๋ฐ ์ˆ˜๋ ด
   - ๊ฒฝ๊ณ„์กฐ๊ฑด ์„ค์ •
   - ๋‚œ๋ฅ˜ ๋ชจ๋ธ ์„ ํƒ
   - ์ˆ˜์น˜ ์•ˆ์ •์„ฑ (CFL ์กฐ๊ฑด)

๋‹ค์Œ ๋ ˆ์Šจ์—์„œ๋Š” ๋น„์••์ถ•์„ฑ ์œ ๋™์˜ ๋Œ€ํ‘œ์  ๋ฌธ์ œ์ธ Lid-Driven Cavity์™€ SIMPLE ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

to navigate between lessons