06_finite_difference.py

Download
python 528 lines 14.4 KB
  1"""
  2์œ ํ•œ ์ฐจ๋ถ„๋ฒ• (Finite Difference Method)
  3Finite Difference Methods for PDEs
  4
  5ํŽธ๋ฏธ๋ถ„๋ฐฉ์ •์‹(PDE)์„ ์ˆ˜์น˜์ ์œผ๋กœ ํ‘ธ๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  6"""
  7
  8import numpy as np
  9import matplotlib.pyplot as plt
 10from typing import Callable, Tuple
 11
 12
 13# =============================================================================
 14# 1. 1D ์—ด ๋ฐฉ์ •์‹ (Heat Equation)
 15# =============================================================================
 16def heat_equation_explicit(
 17    L: float,
 18    T: float,
 19    nx: int,
 20    nt: int,
 21    alpha: float,
 22    initial_condition: Callable[[np.ndarray], np.ndarray],
 23    boundary_left: float = 0,
 24    boundary_right: float = 0
 25) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
 26    """
 27    1D ์—ด ๋ฐฉ์ •์‹ (๋ช…์‹œ์  ๋ฐฉ๋ฒ•)
 28
 29    โˆ‚u/โˆ‚t = ฮฑ โˆ‚ยฒu/โˆ‚xยฒ
 30
 31    FTCS (Forward Time, Central Space):
 32    u(i,n+1) = u(i,n) + r[u(i+1,n) - 2u(i,n) + u(i-1,n)]
 33    ์—ฌ๊ธฐ์„œ r = ฮฑ*dt/dxยฒ
 34
 35    ์•ˆ์ •์„ฑ ์กฐ๊ฑด: r โ‰ค 0.5
 36
 37    Args:
 38        L: ๊ณต๊ฐ„ ์˜์—ญ [0, L]
 39        T: ์‹œ๊ฐ„ ์˜์—ญ [0, T]
 40        nx: ๊ณต๊ฐ„ ๊ฒฉ์ž ์ˆ˜
 41        nt: ์‹œ๊ฐ„ ์Šคํ… ์ˆ˜
 42        alpha: ์—ดํ™•์‚ฐ ๊ณ„์ˆ˜
 43        initial_condition: ์ดˆ๊ธฐ ์กฐ๊ฑด ํ•จ์ˆ˜ u(x, 0)
 44        boundary_left, boundary_right: ๊ฒฝ๊ณ„ ์กฐ๊ฑด
 45
 46    Returns:
 47        (x ๋ฐฐ์—ด, t ๋ฐฐ์—ด, u ๋ฐฐ์—ด)
 48    """
 49    dx = L / (nx - 1)
 50    dt = T / nt
 51    r = alpha * dt / dx**2
 52
 53    print(f"dx={dx:.4f}, dt={dt:.6f}, r={r:.4f}")
 54    if r > 0.5:
 55        print(f"๊ฒฝ๊ณ : ์•ˆ์ •์„ฑ ์กฐ๊ฑด ์œ„๋ฐ˜ (r={r:.4f} > 0.5)")
 56
 57    x = np.linspace(0, L, nx)
 58    t = np.linspace(0, T, nt + 1)
 59    u = np.zeros((nt + 1, nx))
 60
 61    # ์ดˆ๊ธฐ ์กฐ๊ฑด
 62    u[0, :] = initial_condition(x)
 63
 64    # ๊ฒฝ๊ณ„ ์กฐ๊ฑด
 65    u[:, 0] = boundary_left
 66    u[:, -1] = boundary_right
 67
 68    # ์‹œ๊ฐ„ ์ „์ง„
 69    for n in range(nt):
 70        for i in range(1, nx - 1):
 71            u[n + 1, i] = u[n, i] + r * (u[n, i + 1] - 2 * u[n, i] + u[n, i - 1])
 72
 73    return x, t, u
 74
 75
 76def heat_equation_implicit(
 77    L: float,
 78    T: float,
 79    nx: int,
 80    nt: int,
 81    alpha: float,
 82    initial_condition: Callable[[np.ndarray], np.ndarray],
 83    boundary_left: float = 0,
 84    boundary_right: float = 0
 85) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
 86    """
 87    1D ์—ด ๋ฐฉ์ •์‹ (์•”์‹œ์  ๋ฐฉ๋ฒ• - Crank-Nicolson)
 88
 89    ๋ฌด์กฐ๊ฑด ์•ˆ์ •, O(dtยฒ, dxยฒ) ์ •ํ™•๋„
 90    """
 91    dx = L / (nx - 1)
 92    dt = T / nt
 93    r = alpha * dt / (2 * dx**2)
 94
 95    x = np.linspace(0, L, nx)
 96    t = np.linspace(0, T, nt + 1)
 97    u = np.zeros((nt + 1, nx))
 98
 99    # ์ดˆ๊ธฐ ์กฐ๊ฑด
100    u[0, :] = initial_condition(x)
101    u[:, 0] = boundary_left
102    u[:, -1] = boundary_right
103
104    # ์‚ผ๋Œ€๊ฐ ํ–‰๋ ฌ ์„ค์ •
105    n_inner = nx - 2
106    A = np.zeros((n_inner, n_inner))
107    B = np.zeros((n_inner, n_inner))
108
109    for i in range(n_inner):
110        A[i, i] = 1 + 2 * r
111        B[i, i] = 1 - 2 * r
112        if i > 0:
113            A[i, i - 1] = -r
114            B[i, i - 1] = r
115        if i < n_inner - 1:
116            A[i, i + 1] = -r
117            B[i, i + 1] = r
118
119    # ์‹œ๊ฐ„ ์ „์ง„
120    for n in range(nt):
121        # ์šฐ๋ณ€ ๊ณ„์‚ฐ
122        b = B @ u[n, 1:-1]
123        b[0] += r * (u[n + 1, 0] + u[n, 0])
124        b[-1] += r * (u[n + 1, -1] + u[n, -1])
125
126        # ์„ ํ˜• ์‹œ์Šคํ…œ ํ’€๊ธฐ
127        u[n + 1, 1:-1] = np.linalg.solve(A, b)
128
129    return x, t, u
130
131
132# =============================================================================
133# 2. 1D ํŒŒ๋™ ๋ฐฉ์ •์‹ (Wave Equation)
134# =============================================================================
135def wave_equation(
136    L: float,
137    T: float,
138    nx: int,
139    nt: int,
140    c: float,
141    initial_displacement: Callable[[np.ndarray], np.ndarray],
142    initial_velocity: Callable[[np.ndarray], np.ndarray] = None
143) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
144    """
145    1D ํŒŒ๋™ ๋ฐฉ์ •์‹
146
147    โˆ‚ยฒu/โˆ‚tยฒ = cยฒ โˆ‚ยฒu/โˆ‚xยฒ
148
149    ์œ ํ•œ ์ฐจ๋ถ„:
150    u(i,n+1) = 2u(i,n) - u(i,n-1) + sยฒ[u(i+1,n) - 2u(i,n) + u(i-1,n)]
151    ์—ฌ๊ธฐ์„œ s = c*dt/dx (Courant number)
152
153    ์•ˆ์ •์„ฑ ์กฐ๊ฑด: s โ‰ค 1 (CFL ์กฐ๊ฑด)
154    """
155    dx = L / (nx - 1)
156    dt = T / nt
157    s = c * dt / dx
158
159    print(f"Courant number s={s:.4f}")
160    if s > 1:
161        print(f"๊ฒฝ๊ณ : CFL ์กฐ๊ฑด ์œ„๋ฐ˜ (s={s:.4f} > 1)")
162
163    x = np.linspace(0, L, nx)
164    t = np.linspace(0, T, nt + 1)
165    u = np.zeros((nt + 1, nx))
166
167    # ์ดˆ๊ธฐ ์กฐ๊ฑด
168    u[0, :] = initial_displacement(x)
169
170    # ์ฒซ ๋ฒˆ์งธ ์‹œ๊ฐ„ ์Šคํ… (์ดˆ๊ธฐ ์†๋„ ์‚ฌ์šฉ)
171    if initial_velocity is None:
172        initial_velocity = lambda x: np.zeros_like(x)
173
174    v0 = initial_velocity(x)
175    for i in range(1, nx - 1):
176        u[1, i] = (u[0, i] + dt * v0[i] +
177                   0.5 * s**2 * (u[0, i + 1] - 2 * u[0, i] + u[0, i - 1]))
178
179    # ๊ฒฝ๊ณ„ ์กฐ๊ฑด (๊ณ ์ •)
180    u[:, 0] = 0
181    u[:, -1] = 0
182
183    # ์‹œ๊ฐ„ ์ „์ง„
184    for n in range(1, nt):
185        for i in range(1, nx - 1):
186            u[n + 1, i] = (2 * u[n, i] - u[n - 1, i] +
187                          s**2 * (u[n, i + 1] - 2 * u[n, i] + u[n, i - 1]))
188
189    return x, t, u
190
191
192# =============================================================================
193# 3. 2D ๋ผํ”Œ๋ผ์Šค/ํฌ์•„์†ก ๋ฐฉ์ •์‹
194# =============================================================================
195def laplace_2d(
196    Lx: float,
197    Ly: float,
198    nx: int,
199    ny: int,
200    boundary_conditions: dict,
201    max_iter: int = 10000,
202    tol: float = 1e-6
203) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
204    """
205    2D ๋ผํ”Œ๋ผ์Šค ๋ฐฉ์ •์‹ (Jacobi ๋ฐ˜๋ณต๋ฒ•)
206
207    โˆ‡ยฒu = 0  ๋˜๋Š”  โˆ‚ยฒu/โˆ‚xยฒ + โˆ‚ยฒu/โˆ‚yยฒ = 0
208
209    Jacobi ๋ฐ˜๋ณต:
210    u(i,j)_new = 0.25 * [u(i+1,j) + u(i-1,j) + u(i,j+1) + u(i,j-1)]
211
212    Args:
213        boundary_conditions: {'top': val, 'bottom': val, 'left': val, 'right': val}
214    """
215    dx = Lx / (nx - 1)
216    dy = Ly / (ny - 1)
217
218    x = np.linspace(0, Lx, nx)
219    y = np.linspace(0, Ly, ny)
220
221    u = np.zeros((ny, nx))
222
223    # ๊ฒฝ๊ณ„ ์กฐ๊ฑด ์„ค์ •
224    bc = boundary_conditions
225    if callable(bc.get('top')):
226        u[-1, :] = bc['top'](x)
227    else:
228        u[-1, :] = bc.get('top', 0)
229
230    if callable(bc.get('bottom')):
231        u[0, :] = bc['bottom'](x)
232    else:
233        u[0, :] = bc.get('bottom', 0)
234
235    if callable(bc.get('left')):
236        u[:, 0] = bc['left'](y)
237    else:
238        u[:, 0] = bc.get('left', 0)
239
240    if callable(bc.get('right')):
241        u[:, -1] = bc['right'](y)
242    else:
243        u[:, -1] = bc.get('right', 0)
244
245    # Jacobi ๋ฐ˜๋ณต
246    for iteration in range(max_iter):
247        u_old = u.copy()
248
249        for i in range(1, ny - 1):
250            for j in range(1, nx - 1):
251                u[i, j] = 0.25 * (u_old[i + 1, j] + u_old[i - 1, j] +
252                                  u_old[i, j + 1] + u_old[i, j - 1])
253
254        # ์ˆ˜๋ ด ์ฒดํฌ
255        error = np.max(np.abs(u - u_old))
256        if error < tol:
257            print(f"์ˆ˜๋ ด: {iteration + 1}ํšŒ ๋ฐ˜๋ณต, ์˜ค์ฐจ={error:.2e}")
258            break
259
260    return x, y, u
261
262
263def laplace_2d_sor(
264    Lx: float,
265    Ly: float,
266    nx: int,
267    ny: int,
268    boundary_conditions: dict,
269    omega: float = 1.5,
270    max_iter: int = 10000,
271    tol: float = 1e-6
272) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
273    """
274    2D ๋ผํ”Œ๋ผ์Šค ๋ฐฉ์ •์‹ (SOR - Successive Over-Relaxation)
275
276    Jacobi๋ณด๋‹ค ๋น ๋ฅธ ์ˆ˜๋ ด
277
278    Args:
279        omega: ์ด์™„ ์ธ์ž (1 < ฯ‰ < 2 for ๊ฐ€์†)
280    """
281    x = np.linspace(0, Lx, nx)
282    y = np.linspace(0, Ly, ny)
283    u = np.zeros((ny, nx))
284
285    bc = boundary_conditions
286    u[-1, :] = bc.get('top', 0) if not callable(bc.get('top')) else bc['top'](x)
287    u[0, :] = bc.get('bottom', 0) if not callable(bc.get('bottom')) else bc['bottom'](x)
288    u[:, 0] = bc.get('left', 0) if not callable(bc.get('left')) else bc['left'](y)
289    u[:, -1] = bc.get('right', 0) if not callable(bc.get('right')) else bc['right'](y)
290
291    for iteration in range(max_iter):
292        max_diff = 0
293
294        for i in range(1, ny - 1):
295            for j in range(1, nx - 1):
296                u_old = u[i, j]
297                u_gs = 0.25 * (u[i + 1, j] + u[i - 1, j] +
298                              u[i, j + 1] + u[i, j - 1])
299                u[i, j] = (1 - omega) * u_old + omega * u_gs
300                max_diff = max(max_diff, abs(u[i, j] - u_old))
301
302        if max_diff < tol:
303            print(f"SOR ์ˆ˜๋ ด: {iteration + 1}ํšŒ ๋ฐ˜๋ณต")
304            break
305
306    return x, y, u
307
308
309# =============================================================================
310# ์‹œ๊ฐํ™”
311# =============================================================================
312def plot_heat_equation():
313    """์—ด ๋ฐฉ์ •์‹ ์‹œ๊ฐํ™”"""
314    # ์ดˆ๊ธฐ ์กฐ๊ฑด: ๊ฐ€์šด๋ฐ ๋œจ๊ฑฐ์šด ๋ถ€๋ถ„
315    initial = lambda x: np.sin(np.pi * x)
316
317    x, t, u = heat_equation_explicit(
318        L=1.0, T=0.5, nx=51, nt=500, alpha=0.01,
319        initial_condition=initial
320    )
321
322    fig, axes = plt.subplots(1, 2, figsize=(12, 5))
323
324    # ์‹œ๊ณต๊ฐ„ ๋“ฑ๊ณ ์„ 
325    ax = axes[0]
326    X, T_mesh = np.meshgrid(x, t)
327    contour = ax.contourf(X, T_mesh, u, levels=20, cmap='hot')
328    plt.colorbar(contour, ax=ax, label='์˜จ๋„')
329    ax.set_xlabel('์œ„์น˜ x')
330    ax.set_ylabel('์‹œ๊ฐ„ t')
331    ax.set_title('์—ด ๋ฐฉ์ •์‹ ํ•ด')
332
333    # ์‹œ๊ฐ„๋ณ„ ํ”„๋กœํŒŒ์ผ
334    ax = axes[1]
335    times_to_plot = [0, len(t)//4, len(t)//2, 3*len(t)//4, -1]
336    for idx in times_to_plot:
337        ax.plot(x, u[idx, :], label=f't={t[idx]:.3f}')
338    ax.set_xlabel('์œ„์น˜ x')
339    ax.set_ylabel('์˜จ๋„ u')
340    ax.set_title('์‹œ๊ฐ„๋ณ„ ์˜จ๋„ ๋ถ„ํฌ')
341    ax.legend()
342    ax.grid(True)
343
344    plt.tight_layout()
345    plt.savefig('/opt/projects/01_Personal/03_Study/Numerical_Simulation/examples/heat_equation.png', dpi=150)
346    plt.close()
347    print("๊ทธ๋ž˜ํ”„ ์ €์žฅ: heat_equation.png")
348
349
350def plot_wave_equation():
351    """ํŒŒ๋™ ๋ฐฉ์ •์‹ ์‹œ๊ฐํ™”"""
352    # ์ดˆ๊ธฐ ๋ณ€์œ„: ๊ฐ€์šฐ์‹œ์•ˆ ํŽ„์Šค
353    initial = lambda x: np.exp(-100 * (x - 0.5)**2)
354
355    x, t, u = wave_equation(
356        L=1.0, T=2.0, nx=101, nt=400, c=1.0,
357        initial_displacement=initial
358    )
359
360    fig, axes = plt.subplots(1, 2, figsize=(12, 5))
361
362    # ์‹œ๊ณต๊ฐ„ ๋“ฑ๊ณ ์„ 
363    ax = axes[0]
364    X, T_mesh = np.meshgrid(x, t)
365    contour = ax.contourf(X, T_mesh, u, levels=20, cmap='RdBu')
366    plt.colorbar(contour, ax=ax, label='๋ณ€์œ„')
367    ax.set_xlabel('์œ„์น˜ x')
368    ax.set_ylabel('์‹œ๊ฐ„ t')
369    ax.set_title('ํŒŒ๋™ ๋ฐฉ์ •์‹ ํ•ด')
370
371    # ์Šค๋ƒ…์ƒท
372    ax = axes[1]
373    for idx in [0, 50, 100, 150, 200]:
374        ax.plot(x, u[idx, :], label=f't={t[idx]:.2f}')
375    ax.set_xlabel('์œ„์น˜ x')
376    ax.set_ylabel('๋ณ€์œ„ u')
377    ax.set_title('์‹œ๊ฐ„๋ณ„ ํŒŒ๋™ ํ˜•ํƒœ')
378    ax.legend()
379    ax.grid(True)
380
381    plt.tight_layout()
382    plt.savefig('/opt/projects/01_Personal/03_Study/Numerical_Simulation/examples/wave_equation.png', dpi=150)
383    plt.close()
384    print("๊ทธ๋ž˜ํ”„ ์ €์žฅ: wave_equation.png")
385
386
387def plot_laplace_2d():
388    """2D ๋ผํ”Œ๋ผ์Šค ๋ฐฉ์ •์‹ ์‹œ๊ฐํ™”"""
389    bc = {
390        'top': lambda x: np.sin(np.pi * x),
391        'bottom': 0,
392        'left': 0,
393        'right': 0
394    }
395
396    x, y, u = laplace_2d(
397        Lx=1.0, Ly=1.0, nx=51, ny=51,
398        boundary_conditions=bc,
399        tol=1e-5
400    )
401
402    fig, axes = plt.subplots(1, 2, figsize=(12, 5))
403
404    X, Y = np.meshgrid(x, y)
405
406    # ๋“ฑ๊ณ ์„ 
407    ax = axes[0]
408    contour = ax.contourf(X, Y, u, levels=20, cmap='viridis')
409    plt.colorbar(contour, ax=ax)
410    ax.set_xlabel('x')
411    ax.set_ylabel('y')
412    ax.set_title('๋ผํ”Œ๋ผ์Šค ๋ฐฉ์ •์‹ ํ•ด')
413    ax.set_aspect('equal')
414
415    # 3D ํ‘œ๋ฉด
416    ax = axes[1]
417    ax = fig.add_subplot(1, 2, 2, projection='3d')
418    ax.plot_surface(X, Y, u, cmap='viridis', alpha=0.8)
419    ax.set_xlabel('x')
420    ax.set_ylabel('y')
421    ax.set_zlabel('u')
422    ax.set_title('3D ํ‘œ๋ฉด')
423
424    plt.tight_layout()
425    plt.savefig('/opt/projects/01_Personal/03_Study/Numerical_Simulation/examples/laplace_2d.png', dpi=150)
426    plt.close()
427    print("๊ทธ๋ž˜ํ”„ ์ €์žฅ: laplace_2d.png")
428
429
430# =============================================================================
431# ํ…Œ์ŠคํŠธ
432# =============================================================================
433def main():
434    print("=" * 60)
435    print("์œ ํ•œ ์ฐจ๋ถ„๋ฒ• (Finite Difference Method)")
436    print("=" * 60)
437
438    # 1. ์—ด ๋ฐฉ์ •์‹
439    print("\n[1] 1D ์—ด ๋ฐฉ์ •์‹")
440    print("-" * 40)
441
442    initial = lambda x: np.sin(np.pi * x)
443
444    print("๋ช…์‹œ์  ๋ฐฉ๋ฒ• (FTCS):")
445    x, t, u_explicit = heat_equation_explicit(
446        L=1.0, T=0.1, nx=21, nt=100, alpha=0.1,
447        initial_condition=initial
448    )
449    print(f"  t=0.1์—์„œ ์ค‘์•™ ์˜จ๋„: {u_explicit[-1, 10]:.6f}")
450
451    print("\n์•”์‹œ์  ๋ฐฉ๋ฒ• (Crank-Nicolson):")
452    x, t, u_implicit = heat_equation_implicit(
453        L=1.0, T=0.1, nx=21, nt=100, alpha=0.1,
454        initial_condition=initial
455    )
456    print(f"  t=0.1์—์„œ ์ค‘์•™ ์˜จ๋„: {u_implicit[-1, 10]:.6f}")
457
458    # ํ•ด์„ํ•ด: u = exp(-ฯ€ยฒฮฑt) sin(ฯ€x)
459    exact = np.exp(-np.pi**2 * 0.1 * 0.1) * np.sin(np.pi * 0.5)
460    print(f"  ํ•ด์„ํ•ด: {exact:.6f}")
461
462    # 2. ํŒŒ๋™ ๋ฐฉ์ •์‹
463    print("\n[2] 1D ํŒŒ๋™ ๋ฐฉ์ •์‹")
464    print("-" * 40)
465
466    initial_wave = lambda x: np.sin(np.pi * x)
467
468    x, t, u_wave = wave_equation(
469        L=1.0, T=2.0, nx=51, nt=200, c=1.0,
470        initial_displacement=initial_wave
471    )
472    print(f"์ฃผ๊ธฐ T = 2L/c = 2.0")
473    print(f"t=2.0์—์„œ ์ค‘์•™ ๋ณ€์œ„: {u_wave[-1, 25]:.6f}")
474    print(f"(์ดˆ๊ธฐ๊ฐ’๊ณผ ๊ฐ™์•„์•ผ ํ•จ: {initial_wave(0.5):.6f})")
475
476    # 3. 2D ๋ผํ”Œ๋ผ์Šค ๋ฐฉ์ •์‹
477    print("\n[3] 2D ๋ผํ”Œ๋ผ์Šค ๋ฐฉ์ •์‹")
478    print("-" * 40)
479
480    bc = {'top': 100, 'bottom': 0, 'left': 0, 'right': 0}
481
482    print("Jacobi ๋ฐ˜๋ณต:")
483    x, y, u_jacobi = laplace_2d(1.0, 1.0, 31, 31, bc, tol=1e-4)
484
485    print("\nSOR (ฯ‰=1.5):")
486    x, y, u_sor = laplace_2d_sor(1.0, 1.0, 31, 31, bc, omega=1.5, tol=1e-4)
487
488    print(f"\n์ค‘์‹ฌ์  ์˜จ๋„: {u_jacobi[15, 15]:.4f}")
489
490    # ์‹œ๊ฐํ™”
491    try:
492        plot_heat_equation()
493        plot_wave_equation()
494        plot_laplace_2d()
495    except Exception as e:
496        print(f"๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ ์‹คํŒจ: {e}")
497
498    print("\n" + "=" * 60)
499    print("์œ ํ•œ ์ฐจ๋ถ„๋ฒ• ์ •๋ฆฌ")
500    print("=" * 60)
501    print("""
502    PDE ์œ ํ˜•๊ณผ ๋ฐฉ๋ฒ•:
503
504    | PDE ์œ ํ˜•    | ๋Œ€ํ‘œ ์˜ˆ      | ๊ถŒ์žฅ ๋ฐฉ๋ฒ•              |
505    |------------|-------------|----------------------|
506    | ํฌ๋ฌผ์„ ํ˜•    | ์—ด ๋ฐฉ์ •์‹    | FTCS, Crank-Nicolson |
507    | ์Œ๊ณก์„ ํ˜•    | ํŒŒ๋™ ๋ฐฉ์ •์‹  | ์ค‘์‹ฌ์ฐจ๋ถ„, Lax-Wendroff|
508    | ํƒ€์›ํ˜•      | ๋ผํ”Œ๋ผ์Šค     | Jacobi, GS, SOR      |
509
510    ์•ˆ์ •์„ฑ ์กฐ๊ฑด:
511    - ์—ด ๋ฐฉ์ •์‹ (๋ช…์‹œ์ ): r = ฮฑฮ”t/ฮ”xยฒ โ‰ค 0.5
512    - ํŒŒ๋™ ๋ฐฉ์ •์‹: CFL = cฮ”t/ฮ”x โ‰ค 1
513
514    ์ฐจ๋ถ„ ๊ทผ์‚ฌ:
515    - ์ „์ง„ ์ฐจ๋ถ„: โˆ‚u/โˆ‚t โ‰ˆ [u(t+ฮ”t) - u(t)] / ฮ”t
516    - ํ›„์ง„ ์ฐจ๋ถ„: โˆ‚u/โˆ‚t โ‰ˆ [u(t) - u(t-ฮ”t)] / ฮ”t
517    - ์ค‘์‹ฌ ์ฐจ๋ถ„: โˆ‚ยฒu/โˆ‚xยฒ โ‰ˆ [u(x+ฮ”x) - 2u(x) + u(x-ฮ”x)] / ฮ”xยฒ
518
519    ์‹ค๋ฌด:
520    - scipy.ndimage: ๊ฐ„๋‹จํ•œ ํ•„ํ„ฐ๋ง
521    - FEniCS, FiPy: Python PDE ํ”„๋ ˆ์ž„์›Œํฌ
522    - OpenFOAM: CFD (์œ ํ•œ ์ฒด์ ๋ฒ•)
523    """)
524
525
526if __name__ == "__main__":
527    main()