09. ์ด๋ฐฉ์ ์ (Heat Equation)
09. ์ด๋ฐฉ์ ์ (Heat Equation)¶
ํ์ต ๋ชฉํ¶
- 1D/2D ์ด๋ฐฉ์ ์์ ๋ฌผ๋ฆฌ์ ์๋ฏธ ์ดํด
- FTCS (Forward Time Central Space) ์ํด๋ฒ ๊ตฌํ
- BTCS (Backward Time Central Space) ์ํด๋ฒ ๊ตฌํ
- Crank-Nicolson ๋ฐฉ๋ฒ ์ดํด ๋ฐ ๊ตฌํ
- ๋ค์ํ ๊ฒฝ๊ณ์กฐ๊ฑด ์ฒ๋ฆฌ ๋ฐฉ๋ฒ ํ์ต
1. ์ด๋ฐฉ์ ์ ์ด๋ก ¶
1.1 ๋ฌผ๋ฆฌ์ ๋ฐฐ๊ฒฝ¶
์ด๋ฐฉ์ ์์ ์ด์ ๋ ํ์์ ๊ธฐ์ ํ๋ ํฌ๋ฌผ์ ํ PDE์ ๋๋ค.
1D ์ด๋ฐฉ์ ์:
โu/โt = ฮฑ ยท โยฒu/โxยฒ
์ฌ๊ธฐ์:
- u(x,t): ์จ๋
- ฮฑ: ์ดํ์ฐ๊ณ์ (thermal diffusivity)
- x: ๊ณต๊ฐ ์ขํ
- t: ์๊ฐ
1.2 ์ดํ์ฐ๊ณ์¶
"""
์ดํ์ฐ๊ณ์ ฮฑ = k / (ฯยทc)
์ฌ๊ธฐ์:
- k: ์ด์ ๋๋ (W/mยทK)
- ฯ: ๋ฐ๋ (kg/mยณ)
- c: ๋น์ด (J/kgยทK)
"""
# ๋ฌผ์ง๋ณ ์ดํ์ฐ๊ณ์ (mยฒ/s)
thermal_diffusivity = {
'๊ตฌ๋ฆฌ': 1.11e-4,
'์๋ฃจ๋ฏธ๋': 9.7e-5,
'์ฒ ': 2.3e-5,
'์ฝํฌ๋ฆฌํธ': 7.5e-7,
'๋ฌผ': 1.43e-7,
'๊ณต๊ธฐ': 2.2e-5,
}
for material, alpha in thermal_diffusivity.items():
print(f"{material}: ฮฑ = {alpha:.2e} mยฒ/s")
1.3 ํด์ํด (๋ถ๋ฆฌ๋ณ์๋ฒ)¶
๊ฒฝ๊ณ์กฐ๊ฑด u(0,t) = u(L,t) = 0, ์ด๊ธฐ์กฐ๊ฑด u(x,0) = f(x)์ธ ๊ฒฝ์ฐ:
u(x,t) = ฮฃ Bโ ยท sin(nฯx/L) ยท exp(-ฮฑ(nฯ/L)ยฒt)
Bโ = (2/L) โซโ^L f(x)ยทsin(nฯx/L) dx
import numpy as np
import matplotlib.pyplot as plt
def exact_solution_heat(x, t, alpha, L, n_terms=50):
"""
์ด๋ฐฉ์ ์ ํด์ํด (ํธ๋ฆฌ์ ๊ธ์)
์ด๊ธฐ์กฐ๊ฑด: u(x,0) = sin(ฯx/L) (์ฒซ ๋ฒ์งธ ๋ชจ๋๋ง)
๊ฒฝ๊ณ์กฐ๊ฑด: u(0,t) = u(L,t) = 0
"""
# ๋จ์ ์ด๊ธฐ์กฐ๊ฑด์ ๊ฒฝ์ฐ
return np.sin(np.pi * x / L) * np.exp(-alpha * (np.pi / L)**2 * t)
# ์๊ฐํ
L = 1.0
alpha = 0.01
x = np.linspace(0, L, 101)
fig, ax = plt.subplots(figsize=(10, 6))
times = [0, 0.5, 1.0, 2.0, 5.0]
for t in times:
u = exact_solution_heat(x, t, alpha, L)
ax.plot(x, u, label=f't = {t}')
ax.set_xlabel('x')
ax.set_ylabel('u(x,t)')
ax.set_title('1D ์ด๋ฐฉ์ ์ ํด์ํด (์๊ฐ์ ๋ฐ๋ฅธ ๋ณํ)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
# plt.savefig('heat_exact.png', dpi=150)
# plt.show()
2. FTCS ์ํด๋ฒ (Explicit Scheme)¶
2.1 ์ด์ฐํ¶
FTCS = Forward Time, Central Space
์๊ฐ: ์ ๋ฐฉ์ฐจ๋ถ
โu/โt โ (u_i^{n+1} - u_i^n) / ฮt
๊ณต๊ฐ: ์ค์ฌ์ฐจ๋ถ
โยฒu/โxยฒ โ (u_{i+1}^n - 2u_i^n + u_{i-1}^n) / ฮxยฒ
๊ฒฐํฉ:
u_i^{n+1} = u_i^n + rยท(u_{i+1}^n - 2u_i^n + u_{i-1}^n)
์ฌ๊ธฐ์ r = ฮฑยทฮt/ฮxยฒ (์์ ์กฐ๊ฑด: r โค 0.5)
2.2 FTCS ์คํ ์ค ์๊ฐํ¶
์๊ฐ n+1: [i]
โ
์๊ฐ n: [i-1]--[i]--[i+1]
2.3 FTCS ๊ตฌํ¶
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
class HeatEquation1D_FTCS:
"""
1D ์ด๋ฐฉ์ ์ FTCS ์ํด๋ฒ
โu/โt = ฮฑ ยท โยฒu/โxยฒ
"""
def __init__(self, L=1.0, alpha=0.01, nx=51, T=1.0, safety=0.4):
"""
Parameters:
-----------
L : float - ์์ญ ๊ธธ์ด
alpha : float - ์ดํ์ฐ๊ณ์
nx : int - ๊ณต๊ฐ ๊ฒฉ์์ ์
T : float - ์ต์ข
์๊ฐ
safety : float - CFL ์์ ๊ณ์ (0 < safety โค 0.5)
"""
self.L = L
self.alpha = alpha
self.nx = nx
self.T = T
# ๊ฒฉ์ ์์ฑ
self.dx = L / (nx - 1)
self.x = np.linspace(0, L, nx)
# ์์ ์กฐ๊ฑด์ ๋ฐ๋ฅธ ์๊ฐ ๊ฐ๊ฒฉ ๊ฒฐ์
self.dt = safety * self.dx**2 / alpha
self.nt = int(np.ceil(T / self.dt))
self.dt = T / self.nt # ์ ํํ T์ ๋๋ฌํ๋๋ก ์กฐ์
self.r = alpha * self.dt / self.dx**2
print(f"FTCS ์ด๋ฐฉ์ ์ ์ค์ ")
print(f" dx = {self.dx:.4f}, dt = {self.dt:.6f}")
print(f" r = ฮฑยทdt/dxยฒ = {self.r:.4f}")
print(f" ์๊ฐ ์คํ
์: {self.nt}")
print(f" ์์ ์ฑ: {'OK' if self.r <= 0.5 else 'WARNING!'}")
def set_initial_condition(self, func):
"""์ด๊ธฐ์กฐ๊ฑด ์ค์ """
self.u = func(self.x)
self.u0 = self.u.copy()
def set_boundary_conditions(self, left_type='dirichlet', left_value=0,
right_type='dirichlet', right_value=0):
"""๊ฒฝ๊ณ์กฐ๊ฑด ์ค์ """
self.bc = {
'left': {'type': left_type, 'value': left_value},
'right': {'type': right_type, 'value': right_value}
}
def apply_bc(self, u):
"""๊ฒฝ๊ณ์กฐ๊ฑด ์ ์ฉ"""
# ์ผ์ชฝ ๊ฒฝ๊ณ
if self.bc['left']['type'] == 'dirichlet':
u[0] = self.bc['left']['value']
elif self.bc['left']['type'] == 'neumann':
# โu/โx = flux => u[0] = u[1] - flux * dx
u[0] = u[1] - self.bc['left']['value'] * self.dx
# ์ค๋ฅธ์ชฝ ๊ฒฝ๊ณ
if self.bc['right']['type'] == 'dirichlet':
u[-1] = self.bc['right']['value']
elif self.bc['right']['type'] == 'neumann':
# โu/โx = flux => u[-1] = u[-2] + flux * dx
u[-1] = u[-2] + self.bc['right']['value'] * self.dx
return u
def step(self):
"""ํ ์๊ฐ ์คํ
์งํ (FTCS)"""
u_new = self.u.copy()
# ๋ด๋ถ์ ์
๋ฐ์ดํธ
u_new[1:-1] = self.u[1:-1] + self.r * (
self.u[2:] - 2*self.u[1:-1] + self.u[:-2]
)
# ๊ฒฝ๊ณ์กฐ๊ฑด ์ ์ฉ
u_new = self.apply_bc(u_new)
self.u = u_new
def solve(self, save_interval=None):
"""์ ์ฒด ์๊ฐ ๊ตฌ๊ฐ ํ์ด"""
if save_interval is None:
save_interval = max(1, self.nt // 100)
history = [self.u.copy()]
times = [0]
for n in range(self.nt):
self.step()
if (n + 1) % save_interval == 0:
history.append(self.u.copy())
times.append((n + 1) * self.dt)
return np.array(times), np.array(history)
def demo_ftcs():
"""FTCS ๋ฐ๋ชจ"""
# ๋ฌธ์ ์ค์
solver = HeatEquation1D_FTCS(L=1.0, alpha=0.01, nx=51, T=2.0, safety=0.4)
# ์ด๊ธฐ์กฐ๊ฑด: ์ฌ์ธํ
solver.set_initial_condition(lambda x: np.sin(np.pi * x))
# ๊ฒฝ๊ณ์กฐ๊ฑด: ์๋ ๊ณ ์
solver.set_boundary_conditions('dirichlet', 0, 'dirichlet', 0)
# ํ์ด
times, history = solver.solve(save_interval=20)
# ํด์ํด์ ๋น๊ต
u_exact = exact_solution_heat(solver.x, times[-1], solver.alpha, solver.L)
# ์๊ฐํ
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ์๊ฐ์ ๋ฐ๋ฅธ ํด
ax1 = axes[0]
colors = plt.cm.viridis(np.linspace(0, 1, len(times)))
for i, (t, u) in enumerate(zip(times[::10], history[::10])):
ax1.plot(solver.x, u, color=colors[i*10] if i*10 < len(colors) else colors[-1],
label=f't={t:.2f}')
ax1.set_xlabel('x')
ax1.set_ylabel('u')
ax1.set_title('FTCS ์ด๋ฐฉ์ ์ ํด')
ax1.legend(loc='upper right')
ax1.grid(True, alpha=0.3)
# ์ต์ข
์๊ฐ์์ ๋น๊ต
ax2 = axes[1]
ax2.plot(solver.x, history[-1], 'b-', label='FTCS ์์นํด', linewidth=2)
ax2.plot(solver.x, u_exact, 'r--', label='ํด์ํด', linewidth=2)
ax2.set_xlabel('x')
ax2.set_ylabel('u')
ax2.set_title(f't = {times[-1]:.2f}์์ ํด์ํด ๋น๊ต')
ax2.legend()
ax2.grid(True, alpha=0.3)
error = np.max(np.abs(history[-1] - u_exact))
print(f"\n์ต์ข
์๊ฐ์์ ์ต๋ ์ค์ฐจ: {error:.2e}")
plt.tight_layout()
plt.savefig('heat_ftcs.png', dpi=150, bbox_inches='tight')
plt.show()
return solver, times, history
# solver, times, history = demo_ftcs()
3. BTCS ์ํด๋ฒ (Implicit Scheme)¶
3.1 ์ด์ฐํ¶
BTCS = Backward Time, Central Space
์๊ฐ: ํ๋ฐฉ์ฐจ๋ถ (n+1 ์์ ์์ ํ๊ฐ)
โu/โt โ (u_i^{n+1} - u_i^n) / ฮt
๊ณต๊ฐ: ์ค์ฌ์ฐจ๋ถ (n+1 ์์ ์์)
โยฒu/โxยฒ โ (u_{i+1}^{n+1} - 2u_i^{n+1} + u_{i-1}^{n+1}) / ฮxยฒ
์ ๋ฆฌ:
-rยทu_{i-1}^{n+1} + (1+2r)ยทu_i^{n+1} - rยทu_{i+1}^{n+1} = u_i^n
3.2 ํ๋ ฌ ํํ¶
A ยท u^{n+1} = u^n
์ฌ๊ธฐ์ A๋ ์ผ์ค๋๊ฐ ํ๋ ฌ:
| 1+2r -r 0 ... |
| -r 1+2r -r ... |
A = | 0 -r 1+2r ... |
| ... -r |
| -r 1+2r |
3.3 BTCS ๊ตฌํ¶
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
class HeatEquation1D_BTCS:
"""
1D ์ด๋ฐฉ์ ์ BTCS ์ํด๋ฒ
๋ฌด์กฐ๊ฑด ์์ (unconditionally stable)
"""
def __init__(self, L=1.0, alpha=0.01, nx=51, T=1.0, nt=100):
"""
Parameters:
-----------
L : float - ์์ญ ๊ธธ์ด
alpha : float - ์ดํ์ฐ๊ณ์
nx : int - ๊ณต๊ฐ ๊ฒฉ์์ ์
T : float - ์ต์ข
์๊ฐ
nt : int - ์๊ฐ ์คํ
์
"""
self.L = L
self.alpha = alpha
self.nx = nx
self.T = T
self.nt = nt
# ๊ฒฉ์ ์์ฑ
self.dx = L / (nx - 1)
self.dt = T / nt
self.x = np.linspace(0, L, nx)
self.r = alpha * self.dt / self.dx**2
print(f"BTCS ์ด๋ฐฉ์ ์ ์ค์ ")
print(f" dx = {self.dx:.4f}, dt = {self.dt:.6f}")
print(f" r = ฮฑยทdt/dxยฒ = {self.r:.4f}")
print(f" BTCS๋ ๋ฌด์กฐ๊ฑด ์์ (r์ ์ ํ ์์)")
# ํ๋ ฌ A ์์ฑ (๋ด๋ถ์ ๋ง)
self._build_matrix()
def _build_matrix(self):
"""BTCS ํ๋ ฌ ์์ฑ"""
n = self.nx - 2 # ๋ด๋ถ์ ์
main_diag = (1 + 2*self.r) * np.ones(n)
off_diag = -self.r * np.ones(n - 1)
self.A = diags([off_diag, main_diag, off_diag], [-1, 0, 1], format='csr')
def set_initial_condition(self, func):
"""์ด๊ธฐ์กฐ๊ฑด ์ค์ """
self.u = func(self.x)
self.u0 = self.u.copy()
def set_boundary_conditions(self, left_value=0, right_value=0):
"""๋๋ฆฌํด๋ ๊ฒฝ๊ณ์กฐ๊ฑด ์ค์ """
self.u_left = left_value
self.u_right = right_value
def step(self):
"""ํ ์๊ฐ ์คํ
์งํ (BTCS)"""
# ์ฐ๋ณ ๋ฒกํฐ (๋ด๋ถ์ )
b = self.u[1:-1].copy()
# ๊ฒฝ๊ณ์กฐ๊ฑด ๊ธฐ์ฌ
b[0] += self.r * self.u_left
b[-1] += self.r * self.u_right
# ์ ํ ์์คํ
ํ์ด
u_inner = spsolve(self.A, b)
# ์ ์ฒด ํด ์
๋ฐ์ดํธ
self.u[1:-1] = u_inner
self.u[0] = self.u_left
self.u[-1] = self.u_right
def solve(self, save_interval=None):
"""์ ์ฒด ์๊ฐ ๊ตฌ๊ฐ ํ์ด"""
if save_interval is None:
save_interval = max(1, self.nt // 100)
history = [self.u.copy()]
times = [0]
for n in range(self.nt):
self.step()
if (n + 1) % save_interval == 0:
history.append(self.u.copy())
times.append((n + 1) * self.dt)
return np.array(times), np.array(history)
def compare_ftcs_btcs():
"""FTCS vs BTCS ๋น๊ต"""
L = 1.0
alpha = 0.01
nx = 51
T = 2.0
# FTCS (CFL ์ ํ๋จ)
ftcs = HeatEquation1D_FTCS(L, alpha, nx, T, safety=0.4)
ftcs.set_initial_condition(lambda x: np.sin(np.pi * x))
ftcs.set_boundary_conditions('dirichlet', 0, 'dirichlet', 0)
times_ftcs, history_ftcs = ftcs.solve()
# BTCS (ํฐ ์๊ฐ ๊ฐ๊ฒฉ ์ฌ์ฉ ๊ฐ๋ฅ)
btcs = HeatEquation1D_BTCS(L, alpha, nx, T, nt=50) # ํจ์ฌ ์ ์ ์๊ฐ ์คํ
btcs.set_initial_condition(lambda x: np.sin(np.pi * x))
btcs.set_boundary_conditions(0, 0)
times_btcs, history_btcs = btcs.solve()
# ํด์ํด
u_exact = exact_solution_heat(ftcs.x, T, alpha, L)
# ๋น๊ต
print(f"\n๋น๊ต ๊ฒฐ๊ณผ:")
print(f" FTCS ์๊ฐ ์คํ
์: {ftcs.nt}")
print(f" BTCS ์๊ฐ ์คํ
์: {btcs.nt}")
print(f" FTCS ์ต๋ ์ค์ฐจ: {np.max(np.abs(history_ftcs[-1] - u_exact)):.2e}")
print(f" BTCS ์ต๋ ์ค์ฐจ: {np.max(np.abs(history_btcs[-1] - u_exact)):.2e}")
# ์๊ฐํ
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax1 = axes[0]
ax1.plot(ftcs.x, history_ftcs[-1], 'b-', label=f'FTCS (dt={ftcs.dt:.5f})', linewidth=2)
ax1.plot(btcs.x, history_btcs[-1], 'g--', label=f'BTCS (dt={btcs.dt:.4f})', linewidth=2)
ax1.plot(ftcs.x, u_exact, 'r:', label='ํด์ํด', linewidth=2)
ax1.set_xlabel('x')
ax1.set_ylabel('u')
ax1.set_title(f't = {T}์์ ๋น๊ต')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax2 = axes[1]
ax2.semilogy(ftcs.x, np.abs(history_ftcs[-1] - u_exact) + 1e-16, 'b-', label='FTCS ์ค์ฐจ')
ax2.semilogy(btcs.x, np.abs(history_btcs[-1] - u_exact) + 1e-16, 'g--', label='BTCS ์ค์ฐจ')
ax2.set_xlabel('x')
ax2.set_ylabel('|์ค์ฐจ|')
ax2.set_title('์์น ์ค์ฐจ ๋น๊ต')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('heat_ftcs_vs_btcs.png', dpi=150, bbox_inches='tight')
plt.show()
# compare_ftcs_btcs()
4. Crank-Nicolson ๋ฐฉ๋ฒ¶
4.1 ์ด๋ก ¶
Crank-Nicolson = FTCS์ BTCS์ ํ๊ท (2์ฐจ ์ ํ๋)
(u_i^{n+1} - u_i^n) / ฮt = (ฮฑ/2) ยท [(โยฒu/โxยฒ)^n + (โยฒu/โxยฒ)^{n+1}]
์ ๋ฆฌ:
-r/2ยทu_{i-1}^{n+1} + (1+r)ยทu_i^{n+1} - r/2ยทu_{i+1}^{n+1}
= r/2ยทu_{i-1}^n + (1-r)ยทu_i^n + r/2ยทu_{i+1}^n
4.2 ํ๋ ฌ ํํ¶
A ยท u^{n+1} = B ยท u^n
A: (1+r) ๋๊ฐ์ , -r/2 ๋น๋๊ฐ์
B: (1-r) ๋๊ฐ์ , r/2 ๋น๋๊ฐ์
4.3 Crank-Nicolson ๊ตฌํ¶
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
class HeatEquation1D_CrankNicolson:
"""
1D ์ด๋ฐฉ์ ์ Crank-Nicolson ๋ฐฉ๋ฒ
- ๋ฌด์กฐ๊ฑด ์์
- 2์ฐจ ์ ํ๋ (์๊ฐ, ๊ณต๊ฐ ๋ชจ๋)
"""
def __init__(self, L=1.0, alpha=0.01, nx=51, T=1.0, nt=100):
"""
Parameters:
-----------
L : float - ์์ญ ๊ธธ์ด
alpha : float - ์ดํ์ฐ๊ณ์
nx : int - ๊ณต๊ฐ ๊ฒฉ์์ ์
T : float - ์ต์ข
์๊ฐ
nt : int - ์๊ฐ ์คํ
์
"""
self.L = L
self.alpha = alpha
self.nx = nx
self.T = T
self.nt = nt
# ๊ฒฉ์ ์์ฑ
self.dx = L / (nx - 1)
self.dt = T / nt
self.x = np.linspace(0, L, nx)
self.r = alpha * self.dt / self.dx**2
print(f"Crank-Nicolson ์ด๋ฐฉ์ ์ ์ค์ ")
print(f" dx = {self.dx:.4f}, dt = {self.dt:.6f}")
print(f" r = ฮฑยทdt/dxยฒ = {self.r:.4f}")
print(f" 2์ฐจ ์ ํ๋ & ๋ฌด์กฐ๊ฑด ์์ ")
# ํ๋ ฌ ์์ฑ
self._build_matrices()
def _build_matrices(self):
"""Crank-Nicolson ํ๋ ฌ A, B ์์ฑ"""
n = self.nx - 2 # ๋ด๋ถ์ ์
r = self.r
# A ํ๋ ฌ: ์ข๋ณ (์์์ ๋ถ๋ถ)
main_A = (1 + r) * np.ones(n)
off_A = (-r/2) * np.ones(n - 1)
self.A = diags([off_A, main_A, off_A], [-1, 0, 1], format='csr')
# B ํ๋ ฌ: ์ฐ๋ณ (๋ช
์์ ๋ถ๋ถ)
main_B = (1 - r) * np.ones(n)
off_B = (r/2) * np.ones(n - 1)
self.B = diags([off_B, main_B, off_B], [-1, 0, 1], format='csr')
def set_initial_condition(self, func):
"""์ด๊ธฐ์กฐ๊ฑด ์ค์ """
self.u = func(self.x)
self.u0 = self.u.copy()
def set_boundary_conditions(self, left_value=0, right_value=0):
"""๋๋ฆฌํด๋ ๊ฒฝ๊ณ์กฐ๊ฑด ์ค์ """
self.u_left = left_value
self.u_right = right_value
def step(self):
"""ํ ์๊ฐ ์คํ
์งํ (Crank-Nicolson)"""
r = self.r
# ์ฐ๋ณ: Bยทu^n + ๊ฒฝ๊ณ์กฐ๊ฑด ๊ธฐ์ฌ
b = self.B @ self.u[1:-1]
# ๊ฒฝ๊ณ์กฐ๊ฑด ๊ธฐ์ฌ (์ข๋ณ๊ณผ ์ฐ๋ณ ๋ชจ๋์์)
b[0] += (r/2) * (self.u_left + self.u_left) # n๊ณผ n+1 ์์ ์ BC
b[-1] += (r/2) * (self.u_right + self.u_right)
# ์ ํ ์์คํ
ํ์ด
u_inner = spsolve(self.A, b)
# ์ ์ฒด ํด ์
๋ฐ์ดํธ
self.u[1:-1] = u_inner
self.u[0] = self.u_left
self.u[-1] = self.u_right
def solve(self, save_interval=None):
"""์ ์ฒด ์๊ฐ ๊ตฌ๊ฐ ํ์ด"""
if save_interval is None:
save_interval = max(1, self.nt // 100)
history = [self.u.copy()]
times = [0]
for n in range(self.nt):
self.step()
if (n + 1) % save_interval == 0:
history.append(self.u.copy())
times.append((n + 1) * self.dt)
return np.array(times), np.array(history)
def compare_all_schemes():
"""FTCS, BTCS, Crank-Nicolson ๋น๊ต"""
L = 1.0
alpha = 0.01
nx = 51
T = 2.0
# ๋์ผํ (ํฐ) ์๊ฐ ๊ฐ๊ฒฉ์ผ๋ก ๋น๊ต
nt = 40 # FTCS๋ ๋ถ์์ ํ ๊ฒ
# Crank-Nicolson
cn = HeatEquation1D_CrankNicolson(L, alpha, nx, T, nt)
cn.set_initial_condition(lambda x: np.sin(np.pi * x))
cn.set_boundary_conditions(0, 0)
times_cn, history_cn = cn.solve()
# BTCS
btcs = HeatEquation1D_BTCS(L, alpha, nx, T, nt)
btcs.set_initial_condition(lambda x: np.sin(np.pi * x))
btcs.set_boundary_conditions(0, 0)
times_btcs, history_btcs = btcs.solve()
# FTCS (์์ ํ ์ค์ )
ftcs = HeatEquation1D_FTCS(L, alpha, nx, T, safety=0.4)
ftcs.set_initial_condition(lambda x: np.sin(np.pi * x))
ftcs.set_boundary_conditions('dirichlet', 0, 'dirichlet', 0)
times_ftcs, history_ftcs = ftcs.solve()
# ํด์ํด
u_exact = exact_solution_heat(cn.x, T, alpha, L)
# ์ค์ฐจ ๋น๊ต
print(f"\n์ ํ๋ ๋น๊ต (t = {T}):")
print(f" FTCS (dt={ftcs.dt:.5f}, {ftcs.nt} steps): {np.max(np.abs(history_ftcs[-1] - u_exact)):.2e}")
print(f" BTCS (dt={btcs.dt:.4f}, {btcs.nt} steps): {np.max(np.abs(history_btcs[-1] - u_exact)):.2e}")
print(f" C-N (dt={cn.dt:.4f}, {cn.nt} steps): {np.max(np.abs(history_cn[-1] - u_exact)):.2e}")
# ์๊ฐํ
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax1 = axes[0]
ax1.plot(cn.x, history_ftcs[-1], 'b-', label='FTCS', linewidth=2)
ax1.plot(cn.x, history_btcs[-1], 'g--', label='BTCS', linewidth=2)
ax1.plot(cn.x, history_cn[-1], 'm:', label='Crank-Nicolson', linewidth=2)
ax1.plot(cn.x, u_exact, 'r-.', label='ํด์ํด', linewidth=2)
ax1.set_xlabel('x')
ax1.set_ylabel('u')
ax1.set_title(f't = {T}์์ ์ธ ์คํด ๋น๊ต')
ax1.legend()
ax1.grid(True, alpha=0.3)
# ์ ํ๋ ์๋ ด ํ
์คํธ
ax2 = axes[1]
dt_values = []
errors_btcs = []
errors_cn = []
for nt_test in [20, 40, 80, 160, 320]:
dt_test = T / nt_test
dt_values.append(dt_test)
# BTCS
solver = HeatEquation1D_BTCS(L, alpha, nx, T, nt_test)
solver.set_initial_condition(lambda x: np.sin(np.pi * x))
solver.set_boundary_conditions(0, 0)
_, hist = solver.solve()
errors_btcs.append(np.max(np.abs(hist[-1] - u_exact)))
# Crank-Nicolson
solver = HeatEquation1D_CrankNicolson(L, alpha, nx, T, nt_test)
solver.set_initial_condition(lambda x: np.sin(np.pi * x))
solver.set_boundary_conditions(0, 0)
_, hist = solver.solve()
errors_cn.append(np.max(np.abs(hist[-1] - u_exact)))
ax2.loglog(dt_values, errors_btcs, 'gs-', label='BTCS (1์ฐจ)', linewidth=2)
ax2.loglog(dt_values, errors_cn, 'mo-', label='Crank-Nicolson (2์ฐจ)', linewidth=2)
# ๊ธฐ์ค์
dt_ref = np.array(dt_values)
ax2.loglog(dt_ref, 0.5*dt_ref, 'k--', alpha=0.5, label='O(ฮt)')
ax2.loglog(dt_ref, 0.5*dt_ref**2, 'k:', alpha=0.5, label='O(ฮtยฒ)')
ax2.set_xlabel('ฮt')
ax2.set_ylabel('์ต๋ ์ค์ฐจ')
ax2.set_title('์๊ฐ ์ ํ๋ ์๋ ด')
ax2.legend()
ax2.grid(True, alpha=0.3, which='both')
plt.tight_layout()
plt.savefig('heat_scheme_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# compare_all_schemes()
5. 2D ์ด๋ฐฉ์ ์¶
5.1 2D ์ด๋ฐฉ์ ์¶
โu/โt = ฮฑ ยท (โยฒu/โxยฒ + โยฒu/โyยฒ) = ฮฑ ยท โยฒu
5.2 FTCS 2D ๊ตฌํ¶
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
class HeatEquation2D_FTCS:
"""
2D ์ด๋ฐฉ์ ์ FTCS ์ํด๋ฒ
โu/โt = ฮฑ ยท (โยฒu/โxยฒ + โยฒu/โyยฒ)
"""
def __init__(self, Lx=1.0, Ly=1.0, alpha=0.01, nx=51, ny=51, T=0.5, safety=0.2):
"""
Parameters:
-----------
Lx, Ly : float - ์์ญ ํฌ๊ธฐ
alpha : float - ์ดํ์ฐ๊ณ์
nx, ny : int - ๊ฒฉ์์ ์
T : float - ์ต์ข
์๊ฐ
safety : float - CFL ์์ ๊ณ์
"""
self.Lx = Lx
self.Ly = Ly
self.alpha = alpha
self.nx = nx
self.ny = ny
self.T = T
# ๊ฒฉ์ ์์ฑ
self.dx = Lx / (nx - 1)
self.dy = Ly / (ny - 1)
self.x = np.linspace(0, Lx, nx)
self.y = np.linspace(0, Ly, ny)
self.X, self.Y = np.meshgrid(self.x, self.y)
# 2D CFL ์กฐ๊ฑด: r_x + r_y โค 0.5
dt_cfl = safety * 0.5 / (alpha * (1/self.dx**2 + 1/self.dy**2))
self.dt = dt_cfl
self.nt = int(np.ceil(T / self.dt))
self.dt = T / self.nt
self.rx = alpha * self.dt / self.dx**2
self.ry = alpha * self.dt / self.dy**2
print(f"2D FTCS ์ด๋ฐฉ์ ์ ์ค์ ")
print(f" ๊ฒฉ์: {nx} x {ny}")
print(f" dx = {self.dx:.4f}, dy = {self.dy:.4f}, dt = {self.dt:.6f}")
print(f" r_x = {self.rx:.4f}, r_y = {self.ry:.4f}")
print(f" r_x + r_y = {self.rx + self.ry:.4f} (โค 0.5์ด์ด์ผ ์์ )")
def set_initial_condition(self, func):
"""์ด๊ธฐ์กฐ๊ฑด ์ค์ : u(x,y,0) = func(X, Y)"""
self.u = func(self.X, self.Y)
self.u0 = self.u.copy()
def set_boundary_conditions(self, bc_value=0):
"""๋๋ฆฌํด๋ ๊ฒฝ๊ณ์กฐ๊ฑด (๋ชจ๋ ๊ฒฝ๊ณ์์ ๋์ผ ๊ฐ)"""
self.bc_value = bc_value
def apply_bc(self, u):
"""๊ฒฝ๊ณ์กฐ๊ฑด ์ ์ฉ"""
u[0, :] = self.bc_value # ์๋
u[-1, :] = self.bc_value # ์
u[:, 0] = self.bc_value # ์ผ์ชฝ
u[:, -1] = self.bc_value # ์ค๋ฅธ์ชฝ
return u
def step(self):
"""ํ ์๊ฐ ์คํ
์งํ (2D FTCS)"""
u_new = self.u.copy()
# ๋ด๋ถ์ ์
๋ฐ์ดํธ
u_new[1:-1, 1:-1] = self.u[1:-1, 1:-1] + \
self.rx * (self.u[1:-1, 2:] - 2*self.u[1:-1, 1:-1] + self.u[1:-1, :-2]) + \
self.ry * (self.u[2:, 1:-1] - 2*self.u[1:-1, 1:-1] + self.u[:-2, 1:-1])
# ๊ฒฝ๊ณ์กฐ๊ฑด
u_new = self.apply_bc(u_new)
self.u = u_new
def solve(self, save_interval=None):
"""์ ์ฒด ์๊ฐ ๊ตฌ๊ฐ ํ์ด"""
if save_interval is None:
save_interval = max(1, self.nt // 50)
history = [self.u.copy()]
times = [0]
for n in range(self.nt):
self.step()
if (n + 1) % save_interval == 0:
history.append(self.u.copy())
times.append((n + 1) * self.dt)
return np.array(times), history
def demo_heat_2d():
"""2D ์ด๋ฐฉ์ ์ ๋ฐ๋ชจ"""
# ๋ฌธ์ ์ค์
solver = HeatEquation2D_FTCS(Lx=1.0, Ly=1.0, alpha=0.01, nx=51, ny=51, T=0.5)
# ์ด๊ธฐ์กฐ๊ฑด: ๊ฐ์ฐ์์ ์ด์
def ic(X, Y):
x0, y0 = 0.5, 0.5
sigma = 0.1
return np.exp(-((X-x0)**2 + (Y-y0)**2) / (2*sigma**2))
solver.set_initial_condition(ic)
solver.set_boundary_conditions(0)
# ํ์ด
times, history = solver.solve(save_interval=20)
# ์๊ฐํ
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
# ์ ํ๋ ์๊ฐ์์์ ํด
plot_indices = [0, len(times)//4, len(times)//2, 3*len(times)//4, -1]
for idx, i in enumerate(plot_indices[:5]):
if idx < 5:
row = idx // 3
col = idx % 3
ax = axes[row, col]
c = ax.contourf(solver.X, solver.Y, history[i], levels=30, cmap='hot')
plt.colorbar(c, ax=ax)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title(f't = {times[i]:.3f}')
ax.set_aspect('equal')
# ๋น subplot ์ฒ๋ฆฌ
if len(plot_indices) < 6:
axes[1, 2].axis('off')
plt.tight_layout()
plt.savefig('heat_2d.png', dpi=150, bbox_inches='tight')
plt.show()
# ์ค์ฌ์ ์์์ ์๊ฐ ๋ณํ
fig2, ax = plt.subplots(figsize=(10, 5))
center_values = [h[solver.ny//2, solver.nx//2] for h in history]
ax.plot(times, center_values, 'b-', linewidth=2)
ax.set_xlabel('์๊ฐ t')
ax.set_ylabel('u(0.5, 0.5, t)')
ax.set_title('์ค์ฌ์ ์์์ ์จ๋ ๋ณํ')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('heat_2d_center.png', dpi=150, bbox_inches='tight')
plt.show()
return solver, times, history
# solver, times, history = demo_heat_2d()
5.3 2D Crank-Nicolson (ADI ๋ฐฉ๋ฒ)¶
๋๊ท๋ชจ 2D ๋ฌธ์ ์์๋ ADI (Alternating Direction Implicit) ๋ฐฉ๋ฒ์ด ํจ์จ์ ์ ๋๋ค.
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
class HeatEquation2D_ADI:
"""
2D ์ด๋ฐฉ์ ์ ADI (Alternating Direction Implicit) ๋ฐฉ๋ฒ
๊ฐ ์๊ฐ ์คํ
์ ๋ ๋ฐ์คํ
์ผ๋ก ๋๋:
1. x-๋ฐฉํฅ ์์์ , y-๋ฐฉํฅ ๋ช
์์
2. y-๋ฐฉํฅ ์์์ , x-๋ฐฉํฅ ๋ช
์์
๋ฌด์กฐ๊ฑด ์์ + 2์ฐจ ์ ํ๋
"""
def __init__(self, Lx=1.0, Ly=1.0, alpha=0.01, nx=51, ny=51, T=0.5, nt=100):
self.Lx = Lx
self.Ly = Ly
self.alpha = alpha
self.nx = nx
self.ny = ny
self.T = T
self.nt = nt
# ๊ฒฉ์ ์์ฑ
self.dx = Lx / (nx - 1)
self.dy = Ly / (ny - 1)
self.dt = T / nt
self.x = np.linspace(0, Lx, nx)
self.y = np.linspace(0, Ly, ny)
self.X, self.Y = np.meshgrid(self.x, self.y)
self.rx = alpha * self.dt / (2 * self.dx**2)
self.ry = alpha * self.dt / (2 * self.dy**2)
print(f"2D ADI ์ด๋ฐฉ์ ์ ์ค์ ")
print(f" ๊ฒฉ์: {nx} x {ny}")
print(f" r_x = {self.rx:.4f}, r_y = {self.ry:.4f}")
# ํ๋ ฌ ์์ฑ
self._build_matrices()
def _build_matrices(self):
"""ADI ์ผ์ค๋๊ฐ ํ๋ ฌ ์์ฑ"""
# x-๋ฐฉํฅ (๊ฐ y์ ๋ํด)
mx = self.nx - 2
main_x = (1 + 2*self.rx) * np.ones(mx)
off_x = -self.rx * np.ones(mx - 1)
self.Ax = diags([off_x, main_x, off_x], [-1, 0, 1], format='csr')
# y-๋ฐฉํฅ (๊ฐ x์ ๋ํด)
my = self.ny - 2
main_y = (1 + 2*self.ry) * np.ones(my)
off_y = -self.ry * np.ones(my - 1)
self.Ay = diags([off_y, main_y, off_y], [-1, 0, 1], format='csr')
def set_initial_condition(self, func):
"""์ด๊ธฐ์กฐ๊ฑด ์ค์ """
self.u = func(self.X, self.Y)
self.u0 = self.u.copy()
def set_boundary_conditions(self, bc_value=0):
"""๋๋ฆฌํด๋ ๊ฒฝ๊ณ์กฐ๊ฑด"""
self.bc_value = bc_value
def step(self):
"""ํ ์๊ฐ ์คํ
(ADI ๋ ๋ฐ์คํ
)"""
u = self.u
bc = self.bc_value
# ์ค๊ฐ ํด ๋ฐฐ์ด
u_half = np.zeros_like(u)
u_new = np.zeros_like(u)
# ๋ฐ์คํ
1: x-์์์ , y-๋ช
์์
for j in range(1, self.ny - 1):
# y-๋ช
์์ ๋ถ๋ถ (์ฐ๋ณ)
b = u[j, 1:-1] + self.ry * (u[j+1, 1:-1] - 2*u[j, 1:-1] + u[j-1, 1:-1])
# ๊ฒฝ๊ณ์กฐ๊ฑด
b[0] += self.rx * bc
b[-1] += self.rx * bc
# x-์์์ ํ์ด
u_half[j, 1:-1] = spsolve(self.Ax, b)
# ๊ฒฝ๊ณ์กฐ๊ฑด ์ ์ฉ
u_half[0, :] = bc
u_half[-1, :] = bc
u_half[:, 0] = bc
u_half[:, -1] = bc
# ๋ฐ์คํ
2: y-์์์ , x-๋ช
์์
for i in range(1, self.nx - 1):
# x-๋ช
์์ ๋ถ๋ถ (์ฐ๋ณ)
b = u_half[1:-1, i] + self.rx * (u_half[1:-1, i+1] - 2*u_half[1:-1, i] + u_half[1:-1, i-1])
# ๊ฒฝ๊ณ์กฐ๊ฑด
b[0] += self.ry * bc
b[-1] += self.ry * bc
# y-์์์ ํ์ด
u_new[1:-1, i] = spsolve(self.Ay, b)
# ๊ฒฝ๊ณ์กฐ๊ฑด ์ ์ฉ
u_new[0, :] = bc
u_new[-1, :] = bc
u_new[:, 0] = bc
u_new[:, -1] = bc
self.u = u_new
def solve(self, save_interval=None):
"""์ ์ฒด ์๊ฐ ๊ตฌ๊ฐ ํ์ด"""
if save_interval is None:
save_interval = max(1, self.nt // 50)
history = [self.u.copy()]
times = [0]
for n in range(self.nt):
self.step()
if (n + 1) % save_interval == 0:
history.append(self.u.copy())
times.append((n + 1) * self.dt)
return np.array(times), history
def compare_2d_methods():
"""2D FTCS vs ADI ๋น๊ต"""
Lx = Ly = 1.0
alpha = 0.01
nx = ny = 41
T = 0.3
# ์ด๊ธฐ์กฐ๊ฑด
def ic(X, Y):
return np.sin(np.pi * X) * np.sin(np.pi * Y)
# FTCS
ftcs = HeatEquation2D_FTCS(Lx, Ly, alpha, nx, ny, T, safety=0.2)
ftcs.set_initial_condition(ic)
ftcs.set_boundary_conditions(0)
times_ftcs, history_ftcs = ftcs.solve()
# ADI
adi = HeatEquation2D_ADI(Lx, Ly, alpha, nx, ny, T, nt=30)
adi.set_initial_condition(ic)
adi.set_boundary_conditions(0)
times_adi, history_adi = adi.solve()
# ํด์ํด: u = sin(ฯx)sin(ฯy)exp(-2ฮฑฯยฒt)
u_exact = np.sin(np.pi * adi.X) * np.sin(np.pi * adi.Y) * \
np.exp(-2 * alpha * np.pi**2 * T)
print(f"\n๋น๊ต ๊ฒฐ๊ณผ (t = {T}):")
print(f" FTCS ์๊ฐ ์คํ
: {ftcs.nt}")
print(f" ADI ์๊ฐ ์คํ
: {adi.nt}")
print(f" FTCS ์ต๋ ์ค์ฐจ: {np.max(np.abs(history_ftcs[-1] - u_exact)):.2e}")
print(f" ADI ์ต๋ ์ค์ฐจ: {np.max(np.abs(history_adi[-1] - u_exact)):.2e}")
# ์๊ฐํ
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
c1 = axes[0].contourf(adi.X, adi.Y, history_ftcs[-1], levels=30, cmap='hot')
plt.colorbar(c1, ax=axes[0])
axes[0].set_title(f'FTCS (dt={ftcs.dt:.5f})')
axes[0].set_aspect('equal')
c2 = axes[1].contourf(adi.X, adi.Y, history_adi[-1], levels=30, cmap='hot')
plt.colorbar(c2, ax=axes[1])
axes[1].set_title(f'ADI (dt={adi.dt:.4f})')
axes[1].set_aspect('equal')
c3 = axes[2].contourf(adi.X, adi.Y, u_exact, levels=30, cmap='hot')
plt.colorbar(c3, ax=axes[2])
axes[2].set_title('ํด์ํด')
axes[2].set_aspect('equal')
for ax in axes:
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.tight_layout()
plt.savefig('heat_2d_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# compare_2d_methods()
6. ๋ค์ํ ๊ฒฝ๊ณ์กฐ๊ฑด ์ฒ๋ฆฌ¶
6.1 ๋ ธ์ด๋ง ๊ฒฝ๊ณ์กฐ๊ฑด¶
class HeatEquation1D_Neumann:
"""
๋
ธ์ด๋ง ๊ฒฝ๊ณ์กฐ๊ฑด์ ๊ฐ์ง 1D ์ด๋ฐฉ์ ์
โu/โx|_{x=0} = flux_left
โu/โx|_{x=L} = flux_right
"""
def __init__(self, L=1.0, alpha=0.01, nx=51, T=1.0, safety=0.4):
self.L = L
self.alpha = alpha
self.nx = nx
self.T = T
self.dx = L / (nx - 1)
self.dt = safety * self.dx**2 / alpha
self.nt = int(np.ceil(T / self.dt))
self.dt = T / self.nt
self.x = np.linspace(0, L, nx)
self.r = alpha * self.dt / self.dx**2
print(f"๋
ธ์ด๋ง BC ์ด๋ฐฉ์ ์: r = {self.r:.4f}")
def set_initial_condition(self, func):
self.u = func(self.x)
def set_boundary_conditions(self, flux_left=0, flux_right=0):
"""๋
ธ์ด๋ง ๊ฒฝ๊ณ์กฐ๊ฑด ์ค์ """
self.flux_left = flux_left
self.flux_right = flux_right
def step(self):
"""ํ ์๊ฐ ์คํ
(๋
ธ์ด๋ง BC ํฌํจ)"""
u_new = self.u.copy()
# ๋ด๋ถ์
u_new[1:-1] = self.u[1:-1] + self.r * (
self.u[2:] - 2*self.u[1:-1] + self.u[:-2]
)
# ์ผ์ชฝ ๋
ธ์ด๋ง BC: โu/โx = flux_left
# ๊ณ ์คํธ ๋
ธ๋ ๋ฐฉ๋ฒ: u[-1] = u[1] - 2*dx*flux_left
# u_new[0] = u[0] + r*(u[1] - 2*u[0] + u[-1])
# = u[0] + r*(u[1] - 2*u[0] + u[1] - 2*dx*flux_left)
# = u[0] + r*(2*u[1] - 2*u[0] - 2*dx*flux_left)
u_new[0] = self.u[0] + self.r * (
2*self.u[1] - 2*self.u[0] - 2*self.dx*self.flux_left
)
# ์ค๋ฅธ์ชฝ ๋
ธ์ด๋ง BC: โu/โx = flux_right
u_new[-1] = self.u[-1] + self.r * (
2*self.u[-2] - 2*self.u[-1] + 2*self.dx*self.flux_right
)
self.u = u_new
def solve(self):
history = [self.u.copy()]
times = [0]
save_interval = max(1, self.nt // 100)
for n in range(self.nt):
self.step()
if (n + 1) % save_interval == 0:
history.append(self.u.copy())
times.append((n + 1) * self.dt)
return np.array(times), np.array(history)
def demo_neumann_bc():
"""๋
ธ์ด๋ง ๊ฒฝ๊ณ์กฐ๊ฑด ๋ฐ๋ชจ: ๋จ์ด ์๋จ"""
solver = HeatEquation1D_Neumann(L=1.0, alpha=0.01, nx=51, T=5.0)
# ์ด๊ธฐ์กฐ๊ฑด: ์ผ์ชฝ ๋ฐ์ ๋จ๊ฒ๊ณ ์ค๋ฅธ์ชฝ ๋ฐ์ ์ฐจ๊ฐ์
solver.set_initial_condition(lambda x: np.where(x < 0.5, 1.0, 0.0))
# ์๋จ ๋จ์ด (flux = 0)
solver.set_boundary_conditions(flux_left=0, flux_right=0)
times, history = solver.solve()
# ์๋์ง ๋ณด์กด ํ์ธ
energies = [np.trapz(h, solver.x) for h in history]
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ์จ๋ ๋ถํฌ
ax1 = axes[0]
for i in range(0, len(times), len(times)//5):
ax1.plot(solver.x, history[i], label=f't = {times[i]:.2f}')
ax1.set_xlabel('x')
ax1.set_ylabel('u')
ax1.set_title('๋จ์ด ๊ฒฝ๊ณ์กฐ๊ฑด (โu/โx = 0)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# ์ด ์๋์ง
ax2 = axes[1]
ax2.plot(times, energies, 'b-', linewidth=2)
ax2.axhline(y=energies[0], color='r', linestyle='--', label='์ด๊ธฐ ์๋์ง')
ax2.set_xlabel('์๊ฐ t')
ax2.set_ylabel('์ด ์๋์ง (โซu dx)')
ax2.set_title('์๋์ง ๋ณด์กด ํ์ธ')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('heat_neumann.png', dpi=150, bbox_inches='tight')
plt.show()
print(f"์ด๊ธฐ ์๋์ง: {energies[0]:.6f}")
print(f"์ต์ข
์๋์ง: {energies[-1]:.6f}")
print(f"์๋์ง ๋ณํ: {(energies[-1] - energies[0]) / energies[0] * 100:.4f}%")
# demo_neumann_bc()
6.2 ๋ก๋น ๊ฒฝ๊ณ์กฐ๊ฑด¶
def demo_robin_bc():
"""๋ก๋น ๊ฒฝ๊ณ์กฐ๊ฑด: ๋๋ฅ ์ด์ ๋ฌ"""
L = 1.0
alpha = 0.01
nx = 51
T = 2.0
dx = L / (nx - 1)
dt = 0.4 * dx**2 / alpha
nt = int(np.ceil(T / dt))
dt = T / nt
r = alpha * dt / dx**2
x = np.linspace(0, L, nx)
# ์ด๊ธฐ์กฐ๊ฑด: ๊ท ์ผ ์จ๋
u = np.ones(nx)
# ๋ก๋น BC ํ๋ผ๋ฏธํฐ
# -kยทโu/โx = hยท(u - T_inf) at x = 0
# ์ฌ๊ธฐ์ k=์ด์ ๋๋, h=์ด์ ๋ฌ๊ณ์, T_inf=์ฃผ๋ณ์จ๋
k = 1.0
h = 10.0 # ์ด์ ๋ฌ๊ณ์
T_inf = 0.0 # ์ฃผ๋ณ ์จ๋
Bi = h * dx / k # Biot ์
history = [u.copy()]
times = [0]
for n in range(nt):
u_new = u.copy()
# ๋ด๋ถ์
u_new[1:-1] = u[1:-1] + r * (u[2:] - 2*u[1:-1] + u[:-2])
# ์ผ์ชฝ ๋ก๋น BC: h(u - T_inf) + kยทโu/โx = 0
# (u[1] - u[0])/dx = (h/k)(u[0] - T_inf)
# ๊ณ ์คํธ ๋
ธ๋: u[-1] = u[1] - 2*dx*(h/k)*(u[0] - T_inf)
u_new[0] = u[0] + r * (2*u[1] - 2*u[0] - 2*dx*(h/k)*(u[0] - T_inf))
# ์ค๋ฅธ์ชฝ: ๋๋ฆฌํด๋ (๊ณ ์ ์จ๋)
u_new[-1] = 1.0
u = u_new
if (n + 1) % (nt // 50) == 0:
history.append(u.copy())
times.append((n + 1) * dt)
# ์๊ฐํ
fig, ax = plt.subplots(figsize=(10, 6))
for i in range(0, len(times), len(times)//5):
ax.plot(x, history[i], label=f't = {times[i]:.2f}')
ax.set_xlabel('x')
ax.set_ylabel('u')
ax.set_title(f'๋ก๋น ๊ฒฝ๊ณ์กฐ๊ฑด (๋๋ฅ ์ด์ ๋ฌ)\nBiot ์ = {Bi:.2f}')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('heat_robin.png', dpi=150, bbox_inches='tight')
plt.show()
# demo_robin_bc()
7. ์์ฝ¶
์คํด ๋น๊ตํ¶
| ์คํด | ์ ํ๋ | ์์ ์ฑ | ๊ณ์ฐ ๋น์ฉ | ํน์ง |
|---|---|---|---|---|
| FTCS | O(ฮt, ฮxยฒ) | ์กฐ๊ฑด๋ถ (rโค0.5) | ๋ฎ์ | ๋จ์, ๋ช ์์ |
| BTCS | O(ฮt, ฮxยฒ) | ๋ฌด์กฐ๊ฑด | ์ค๊ฐ | ์์์ , ํ๋ ฌ ํ์ด |
| Crank-Nicolson | O(ฮtยฒ, ฮxยฒ) | ๋ฌด์กฐ๊ฑด | ์ค๊ฐ | 2์ฐจ ์ ํ๋ |
| ADI (2D) | O(ฮtยฒ, ฮxยฒ) | ๋ฌด์กฐ๊ฑด | ์ค๊ฐ | ํจ์จ์ ์ธ 2D |
CFL ์กฐ๊ฑด¶
1D FTCS: r = ฮฑยทฮt/ฮxยฒ โค 0.5
2D FTCS: r_x + r_y โค 0.5
๋ค์ ๋จ๊ณ¶
- 10์ฅ: ํ๋๋ฐฉ์ ์ - ์๊ณก์ ํ PDE
- 11์ฅ: ๋ผํ๋ผ์ค/ํฌ์์ก - ํ์ํ PDE
- 12์ฅ: ์ด๋ฅ๋ฐฉ์ ์ - 1์ฐจ ์๊ณก์ ํ PDE
์ฐ์ต๋ฌธ์ ¶
์ฐ์ต 1: FTCS ์์ ์ฑ ์คํ¶
r = 0.3, 0.5, 0.6์์ FTCS๋ฅผ ์คํํ๊ณ ์์ /๋ถ์์ ๋์์ ํ์ธํ์์ค.
์ฐ์ต 2: ์๋ ด ์ฐจ์ ํ์ธ¶
Crank-Nicolson์ ์๊ฐ ์ ํ๋๊ฐ 2์ฐจ์์ ์์น์ ์ผ๋ก ํ์ธํ์์ค.
์ฐ์ต 3: ๋น๊ท ์ง ๊ฒฝ๊ณ์กฐ๊ฑด¶
u(0,t) = 0, u(L,t) = 100์ธ ๊ฒฝ์ฐ์ ์ ์์ํ ํด๋ฅผ ๊ตฌํ์์ค.
์ฐ์ต 4: 2D ์ด๋ฐฉ์ ์¶
๊ฐ์ฐ์์ ์ด๊ธฐ์กฐ๊ฑด์ด ์๋ ์ฌ๊ฐํ ์ด์ ์ด๊ธฐ์กฐ๊ฑด์ผ๋ก 2D ์ด๋ฐฉ์ ์์ ํ์ด๋ณด์์ค.
์ฐธ๊ณ ์๋ฃ¶
- ๊ต์ฌ: LeVeque, "Finite Difference Methods for Ordinary and Partial Differential Equations"
- Python: scipy.sparse, numpy
- ์๊ฐํ: matplotlib.animation