16. FDTD ๊ตฌํ˜„ (FDTD Implementation)

16. FDTD ๊ตฌํ˜„ (FDTD Implementation)

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

  • 1D FDTD์˜ ์™„์ „ํ•œ ๊ตฌํ˜„
  • ์†Œ์Šค ์—ฌ๊ธฐ ๋ฐฉ๋ฒ• (๊ฐ€์šฐ์‹œ์•ˆ ํŽ„์Šค, ์ •ํ˜„ํŒŒ)
  • ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด (Simple ABC, Mur ABC)
  • 2D FDTD (TM, TE ๋ชจ๋“œ)
  • PML (Perfectly Matched Layer) ๊ฐœ๋…

1. 1D FDTD ์™„์ „ ๊ตฌํ˜„

1.1 ๊ธฐ๋ณธ ๊ตฌ์กฐ

1D FDTD ์•Œ๊ณ ๋ฆฌ์ฆ˜:

์ดˆ๊ธฐํ™”:
- ๊ฒฉ์ž ์„ค์ • (Nx, dx, dt)
- ๋ฐฐ์—ด ์ดˆ๊ธฐํ™” (Ey, Hz)
- ๋ฌผ์„ฑ์น˜ ์„ค์ • (ฮต, ฮผ, ฯƒ)

์‹œ๊ฐ„ ๋ฃจํ”„:
for n = 1, 2, ..., Nt:
    1. H ์—…๋ฐ์ดํŠธ: Hz^(n+1/2) = f(Hz^(n-1/2), Ey^n)
    2. ์†Œ์Šค ์ฃผ์ž… (soft/hard)
    3. E ์—…๋ฐ์ดํŠธ: Ey^(n+1) = f(Ey^n, Hz^(n+1/2))
    4. ๊ฒฝ๊ณ„์กฐ๊ฑด ์ ์šฉ (ABC)
    5. ๋ฐ์ดํ„ฐ ๊ธฐ๋ก/์ถœ๋ ฅ
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML

# ๋ฌผ๋ฆฌ ์ƒ์ˆ˜
c0 = 299792458.0  # ๊ด‘์† [m/s]
eps0 = 8.854187817e-12  # ์ง„๊ณต ์œ ์ „์œจ [F/m]
mu0 = 4 * np.pi * 1e-7  # ์ง„๊ณต ํˆฌ์ž์œจ [H/m]
eta0 = np.sqrt(mu0 / eps0)  # ์ง„๊ณต ์ž„ํ”ผ๋˜์Šค [ฮฉ]

class FDTD_1D:
    """1D FDTD ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ"""

    def __init__(self, Nx=200, dx=1e-3, courant=0.99):
        """
        Parameters:
        - Nx: ๊ฒฉ์ž์  ์ˆ˜
        - dx: ๊ณต๊ฐ„ ๊ฐ„๊ฒฉ [m]
        - courant: Courant ์ˆ˜ (โ‰ค 1)
        """
        self.Nx = Nx
        self.dx = dx
        self.dt = courant * dx / c0

        # ํ•„๋“œ ๋ฐฐ์—ด
        self.Ey = np.zeros(Nx)
        self.Hz = np.zeros(Nx)

        # ๋ฌผ์„ฑ์น˜ ๋ฐฐ์—ด (์ƒ๋Œ€๊ฐ’)
        self.eps_r = np.ones(Nx)
        self.mu_r = np.ones(Nx)
        self.sigma = np.zeros(Nx)  # ์ „๊ธฐ ์ „๋„๋„
        self.sigma_m = np.zeros(Nx)  # ์ž๊ธฐ ์ „๋„๋„

        # ABC์šฉ ์ด์ „ ํ•„๋“œ๊ฐ’
        self.Ey_left_prev = [0, 0]
        self.Ey_right_prev = [0, 0]

        # ์‹œ๊ฐ„
        self.time_step = 0

        print(f"1D FDTD ์ดˆ๊ธฐํ™”:")
        print(f"  Nx = {Nx}, dx = {dx*1000:.2f} mm")
        print(f"  dt = {self.dt*1e12:.3f} ps")
        print(f"  Courant S = {courant}")

    def set_material(self, start, end, eps_r=1, sigma=0):
        """์žฌ๋ฃŒ ์˜์—ญ ์„ค์ •"""
        self.eps_r[start:end] = eps_r
        self.sigma[start:end] = sigma

    def compute_coefficients(self):
        """์—…๋ฐ์ดํŠธ ๊ณ„์ˆ˜ ๊ณ„์‚ฐ"""
        eps = eps0 * self.eps_r
        mu = mu0 * self.mu_r

        # E ์—…๋ฐ์ดํŠธ ๊ณ„์ˆ˜ (์†์‹ค ํฌํ•จ)
        self.Ca = (1 - self.sigma * self.dt / (2 * eps)) / \
                  (1 + self.sigma * self.dt / (2 * eps))
        self.Cb = (self.dt / (eps * self.dx)) / \
                  (1 + self.sigma * self.dt / (2 * eps))

        # H ์—…๋ฐ์ดํŠธ ๊ณ„์ˆ˜
        self.Da = (1 - self.sigma_m * self.dt / (2 * mu)) / \
                  (1 + self.sigma_m * self.dt / (2 * mu))
        self.Db = (self.dt / (mu * self.dx)) / \
                  (1 + self.sigma_m * self.dt / (2 * mu))

    def update_H(self):
        """H ํ•„๋“œ ์—…๋ฐ์ดํŠธ"""
        self.Hz[:-1] = (self.Da[:-1] * self.Hz[:-1] -
                       self.Db[:-1] * (self.Ey[1:] - self.Ey[:-1]))

    def update_E(self):
        """E ํ•„๋“œ ์—…๋ฐ์ดํŠธ"""
        self.Ey[1:-1] = (self.Ca[1:-1] * self.Ey[1:-1] -
                        self.Cb[1:-1] * (self.Hz[1:-1] - self.Hz[:-2]))

    def add_source_soft(self, position, value):
        """์†Œํ”„ํŠธ ์†Œ์Šค (์ด ํ•„๋“œ/์‚ฐ๋ž€ ํ•„๋“œ ๊ฒฝ๊ณ„)"""
        self.Ey[position] += value

    def add_source_hard(self, position, value):
        """ํ•˜๋“œ ์†Œ์Šค (๊ฐ•์ œ ์ฃผ์ž…)"""
        self.Ey[position] = value

    def apply_abc_simple(self):
        """๊ฐ„๋‹จํ•œ ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด (1์ฐจ)"""
        # ์ขŒ์ธก ๊ฒฝ๊ณ„
        self.Ey[0] = self.Ey_left_prev[0]
        self.Ey_left_prev[0] = self.Ey_left_prev[1]
        self.Ey_left_prev[1] = self.Ey[1]

        # ์šฐ์ธก ๊ฒฝ๊ณ„
        self.Ey[-1] = self.Ey_right_prev[0]
        self.Ey_right_prev[0] = self.Ey_right_prev[1]
        self.Ey_right_prev[1] = self.Ey[-2]

    def apply_abc_mur(self):
        """Mur 1์ฐจ ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด"""
        coeff = (c0 * self.dt - self.dx) / (c0 * self.dt + self.dx)

        # ์ขŒ์ธก ๊ฒฝ๊ณ„
        self.Ey[0] = self.Ey_left_prev[1] + coeff * (self.Ey[1] - self.Ey_left_prev[0])
        self.Ey_left_prev[0] = self.Ey[0]
        self.Ey_left_prev[1] = self.Ey[1]

        # ์šฐ์ธก ๊ฒฝ๊ณ„
        self.Ey[-1] = self.Ey_right_prev[1] + coeff * (self.Ey[-2] - self.Ey_right_prev[0])
        self.Ey_right_prev[0] = self.Ey[-1]
        self.Ey_right_prev[1] = self.Ey[-2]

    def step(self, source_func=None, source_pos=None, abc_type='mur'):
        """ํ•œ ์‹œ๊ฐ„ ๋‹จ๊ณ„ ์ „์ง„"""
        self.update_H()

        if source_func is not None and source_pos is not None:
            t = self.time_step * self.dt
            self.add_source_soft(source_pos, source_func(t))

        self.update_E()

        if abc_type == 'simple':
            self.apply_abc_simple()
        elif abc_type == 'mur':
            self.apply_abc_mur()
        else:  # PEC
            self.Ey[0] = 0
            self.Ey[-1] = 0

        self.time_step += 1

    def run(self, n_steps, source_func=None, source_pos=None, abc_type='mur',
           record_interval=1):
        """์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰"""
        self.compute_coefficients()

        Ey_history = []
        Hz_history = []

        for n in range(n_steps):
            self.step(source_func, source_pos, abc_type)

            if n % record_interval == 0:
                Ey_history.append(self.Ey.copy())
                Hz_history.append(self.Hz.copy())

        return np.array(Ey_history), np.array(Hz_history)


def gaussian_pulse(t, t0=1e-10, tau=3e-11):
    """๊ฐ€์šฐ์‹œ์•ˆ ํŽ„์Šค ์†Œ์Šค"""
    return np.exp(-((t - t0) / tau) ** 2)

def sinusoidal_source(t, freq=3e9, t0=5e-11, tau=2e-11):
    """๋ณ€์กฐ๋œ ์ •ํ˜„ํŒŒ ์†Œ์Šค"""
    envelope = 1 - np.exp(-((t - t0) / tau) ** 2) if t < t0 else 1
    return envelope * np.sin(2 * np.pi * freq * t)


def demo_1d_fdtd_basic():
    """1D FDTD ๊ธฐ๋ณธ ์‹œ์—ฐ"""

    # ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ƒ์„ฑ
    fdtd = FDTD_1D(Nx=300, dx=1e-3, courant=0.99)

    # ์œ ์ „์ฒด ์Šฌ๋ž˜๋ธŒ ์ถ”๊ฐ€
    fdtd.set_material(150, 200, eps_r=4.0)

    # ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰
    source_pos = 50
    n_steps = 500

    Ey_history, Hz_history = fdtd.run(
        n_steps,
        source_func=gaussian_pulse,
        source_pos=source_pos,
        abc_type='mur',
        record_interval=5
    )

    # ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
    x = np.arange(fdtd.Nx) * fdtd.dx * 1000  # mm

    fig, axes = plt.subplots(2, 3, figsize=(15, 8))

    # ์Šค๋ƒ…์ƒท
    snapshot_indices = [10, 30, 50, 70, 90]

    for idx, snap_idx in enumerate(snapshot_indices):
        if idx < 5:
            ax = axes[idx // 3, idx % 3]
            ax.plot(x, Ey_history[snap_idx], 'b-', linewidth=1.5)

            # ์œ ์ „์ฒด ์˜์—ญ ํ‘œ์‹œ
            ax.axvspan(150 * fdtd.dx * 1000, 200 * fdtd.dx * 1000,
                      alpha=0.2, color='green', label=r'$\epsilon_r=4$')
            ax.axvline(x=source_pos * fdtd.dx * 1000, color='red',
                      linestyle='--', alpha=0.5)

            ax.set_xlabel('x [mm]')
            ax.set_ylabel('Ey')
            ax.set_title(f'Step {snap_idx * 5}')
            ax.grid(True, alpha=0.3)
            ax.set_ylim(-1.2, 1.2)

    # ๋งˆ์ง€๋ง‰ subplot: ์‹œ๊ณต๊ฐ„ ๋‹ค์ด์–ด๊ทธ๋žจ
    ax = axes[1, 2]
    t = np.arange(len(Ey_history)) * 5 * fdtd.dt * 1e9  # ns

    im = ax.pcolormesh(x, t, Ey_history, cmap='RdBu_r', shading='auto',
                      vmin=-0.5, vmax=0.5)
    ax.axvline(x=150 * fdtd.dx * 1000, color='green', linestyle='-', linewidth=2)
    ax.axvline(x=200 * fdtd.dx * 1000, color='green', linestyle='-', linewidth=2)
    ax.set_xlabel('x [mm]')
    ax.set_ylabel('t [ns]')
    ax.set_title('์‹œ๊ณต๊ฐ„ ๋‹ค์ด์–ด๊ทธ๋žจ')
    plt.colorbar(im, ax=ax, label='Ey')

    plt.suptitle('1D FDTD: ์œ ์ „์ฒด ์Šฌ๋ž˜๋ธŒ์—์„œ์˜ ๋ฐ˜์‚ฌ์™€ ํˆฌ๊ณผ', fontsize=14)
    plt.tight_layout()
    plt.savefig('fdtd_1d_dielectric.png', dpi=150, bbox_inches='tight')
    plt.show()

    return fdtd, Ey_history

# fdtd, Ey_history = demo_1d_fdtd_basic()

2. ์†Œ์Šค ์—ฌ๊ธฐ ๋ฐฉ๋ฒ•

2.1 ํ•˜๋“œ ์†Œ์Šค vs ์†Œํ”„ํŠธ ์†Œ์Šค

์†Œ์Šค ์œ ํ˜•:

1. ํ•˜๋“œ ์†Œ์Šค (Hard Source):
   Ey[source_pos] = source_value
   - ํ•ด๋‹น ์ ์˜ ํ•„๋“œ ๊ฐ•์ œ ์„ค์ •
   - ๋ฐ˜์‚ฌํŒŒ๊ฐ€ ์†Œ์Šค์—์„œ ๋‹ค์‹œ ๋ฐ˜์‚ฌ๋จ
   - ๊ฐ„๋‹จํ•˜์ง€๋งŒ ๋น„๋ฌผ๋ฆฌ์  ๋ฐ˜์‚ฌ ๋ฐœ์ƒ

2. ์†Œํ”„ํŠธ ์†Œ์Šค (Soft Source):
   Ey[source_pos] += source_value
   - ๊ธฐ์กด ํ•„๋“œ์— ์†Œ์Šค ์ถ”๊ฐ€
   - ๋ฐ˜์‚ฌํŒŒ๊ฐ€ ์†Œ์Šค๋ฅผ ํ†ต๊ณผ
   - TF/SF ๊ฒฝ๊ณ„์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ

3. TF/SF (Total-Field/Scattered-Field):
   - ์ž…์‚ฌํŒŒ์™€ ์‚ฐ๋ž€ํŒŒ ๋ถ„๋ฆฌ
   - ์ •ํ™•ํ•œ ํ‰๋ฉดํŒŒ ์ฃผ์ž…
   - ์ถ”๊ฐ€ ๋ณด์ •ํ•ญ ํ•„์š”
def source_comparison():
    """ํ•˜๋“œ ์†Œ์Šค์™€ ์†Œํ”„ํŠธ ์†Œ์Šค ๋น„๊ต"""

    fig, axes = plt.subplots(2, 3, figsize=(15, 8))

    for row, source_type in enumerate(['hard', 'soft']):
        # ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ƒ์„ฑ
        fdtd = FDTD_1D(Nx=200, dx=1e-3, courant=0.99)
        fdtd.compute_coefficients()

        source_pos = 50
        n_steps = 300

        # ๋ฐ˜์‚ฌ์ฒด (PEC) ์ถ”๊ฐ€
        fdtd.sigma[150:155] = 1e7

        Ey_history = []

        for n in range(n_steps):
            fdtd.update_H()

            t = n * fdtd.dt
            source = gaussian_pulse(t, t0=5e-11, tau=2e-11)

            if source_type == 'hard':
                fdtd.Ey[source_pos] = source
            else:
                fdtd.Ey[source_pos] += source

            fdtd.update_E()
            fdtd.apply_abc_mur()

            if n % 3 == 0:
                Ey_history.append(fdtd.Ey.copy())

        x = np.arange(fdtd.Nx) * fdtd.dx * 1000

        # ์Šค๋ƒ…์ƒท
        for col, snap_idx in enumerate([20, 50, 80]):
            ax = axes[row, col]
            ax.plot(x, Ey_history[snap_idx], 'b-', linewidth=1.5)
            ax.axvline(x=source_pos * fdtd.dx * 1000, color='red', linestyle='--',
                      label='Source')
            ax.axvspan(150 * fdtd.dx * 1000, 155 * fdtd.dx * 1000,
                      alpha=0.3, color='gray', label='PEC')

            ax.set_xlabel('x [mm]')
            ax.set_ylabel('Ey')
            ax.set_title(f'{source_type.capitalize()} Source, Step {snap_idx * 3}')
            ax.grid(True, alpha=0.3)
            ax.set_ylim(-1.5, 1.5)
            if col == 0:
                ax.legend()

    plt.suptitle('ํ•˜๋“œ ์†Œ์Šค vs ์†Œํ”„ํŠธ ์†Œ์Šค ๋น„๊ต', fontsize=14)
    plt.tight_layout()
    plt.savefig('fdtd_source_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()

# source_comparison()

2.2 ๋‹ค์–‘ํ•œ ์†Œ์Šค ํŒŒํ˜•

def source_waveforms():
    """๋‹ค์–‘ํ•œ ์†Œ์Šค ํŒŒํ˜•"""

    t = np.linspace(0, 0.5e-9, 1000)  # 0.5 ns

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

    # (1) ๊ฐ€์šฐ์‹œ์•ˆ ํŽ„์Šค
    ax1 = axes[0, 0]
    t0, tau = 0.15e-9, 0.03e-9
    pulse = np.exp(-((t - t0) / tau) ** 2)
    ax1.plot(t * 1e9, pulse, 'b-', linewidth=2)
    ax1.set_title('๊ฐ€์šฐ์‹œ์•ˆ ํŽ„์Šค')
    ax1.set_xlabel('t [ns]')
    ax1.set_ylabel('Amplitude')
    ax1.grid(True, alpha=0.3)

    # (2) ๊ฐ€์šฐ์‹œ์•ˆ ๋ฏธ๋ถ„ (Ricker wavelet)
    ax2 = axes[0, 1]
    ricker = -2 * (t - t0) / tau**2 * np.exp(-((t - t0) / tau) ** 2)
    ax2.plot(t * 1e9, ricker, 'r-', linewidth=2)
    ax2.set_title('๊ฐ€์šฐ์‹œ์•ˆ ๋ฏธ๋ถ„ (Ricker Wavelet)')
    ax2.set_xlabel('t [ns]')
    ax2.set_ylabel('Amplitude')
    ax2.grid(True, alpha=0.3)

    # (3) ๋ณ€์กฐ ์ •ํ˜„ํŒŒ
    ax3 = axes[1, 0]
    freq = 10e9  # 10 GHz
    modulated = np.sin(2 * np.pi * freq * t) * np.exp(-((t - t0) / tau) ** 2)
    ax3.plot(t * 1e9, modulated, 'g-', linewidth=1.5)
    ax3.set_title('๋ณ€์กฐ ์ •ํ˜„ํŒŒ (10 GHz)')
    ax3.set_xlabel('t [ns]')
    ax3.set_ylabel('Amplitude')
    ax3.grid(True, alpha=0.3)

    # (4) ์ŠคํŽ™ํŠธ๋Ÿผ
    ax4 = axes[1, 1]
    from scipy.fft import fft, fftfreq

    dt = t[1] - t[0]
    freqs = fftfreq(len(t), dt)
    positive = freqs > 0

    for signal, label, color in [(pulse, 'Gaussian', 'b'),
                                  (ricker, 'Ricker', 'r'),
                                  (modulated, 'Modulated', 'g')]:
        spectrum = np.abs(fft(signal))
        ax4.plot(freqs[positive] * 1e-9, spectrum[positive] / max(spectrum[positive]),
                linewidth=1.5, label=label, color=color)

    ax4.set_xlim(0, 50)
    ax4.set_xlabel('Frequency [GHz]')
    ax4.set_ylabel('Normalized Amplitude')
    ax4.set_title('์ฃผํŒŒ์ˆ˜ ์ŠคํŽ™ํŠธ๋Ÿผ')
    ax4.legend()
    ax4.grid(True, alpha=0.3)

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

# source_waveforms()

3. ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด (ABC)

3.1 Simple ABC

1์ฐจ Simple ABC:

1D ํŒŒ๋™ ๋ฐฉ์ •์‹์˜ ํŠน์„ฑ:
(โˆ‚/โˆ‚t + c โˆ‚/โˆ‚x) Ey = 0  (์šฐ์ธก ์ง„ํ–‰ํŒŒ)
(โˆ‚/โˆ‚t - c โˆ‚/โˆ‚x) Ey = 0  (์ขŒ์ธก ์ง„ํ–‰ํŒŒ)

์ด์‚ฐํ™” (์šฐ์ธก ๊ฒฝ๊ณ„, ์ขŒ์ธก ์ง„ํ–‰ํŒŒ ํก์ˆ˜):
Ey^(n+1)[Nx-1] = Ey^n[Nx-2]

์ด๊ฒƒ์€ S = cฮ”t/ฮ”x = 1 ์ผ ๋•Œ๋งŒ ์ •ํ™•
S โ‰  1 ์ด๋ฉด ๋ฐ˜์‚ฌ ๋ฐœ์ƒ

3.2 Mur ABC

Mur 1์ฐจ ABC (1981):

1D ํŒŒ๋™ ๋ฐฉ์ •์‹์˜ ์œ ํ•œ์ฐจ๋ถ„ ๊ทผ์‚ฌ:

์šฐ์ธก ๊ฒฝ๊ณ„ (x = xmax):
(Ey^(n+1)[Nx-1] - Ey^n[Nx-2]) / (cฮ”t + ฮ”x) =
(Ey^n[Nx-1] - Ey^(n+1)[Nx-2]) / (cฮ”t - ฮ”x)

์ •๋ฆฌ:
Ey^(n+1)[Nx-1] = Ey^n[Nx-2] +
                 (cฮ”t - ฮ”x)/(cฮ”t + ฮ”x) * (Ey^(n+1)[Nx-2] - Ey^n[Nx-1])

์žฅ์ :
- Simple ABC๋ณด๋‹ค ๋„“์€ ์ž…์‚ฌ๊ฐ์—์„œ ์œ ํšจ
- ๊ตฌํ˜„ ๊ฐ„๋‹จ

ํ•œ๊ณ„:
- ์ˆ˜์ง ์ž…์‚ฌ๋งŒ ์ •ํ™•ํžˆ ํก์ˆ˜
- ๋น„์Šค๋“ฌํ•œ ์ž…์‚ฌ์—์„œ ๋ฐ˜์‚ฌ ๋ฐœ์ƒ
def abc_comparison():
    """ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด ๋น„๊ต"""

    abc_types = ['pec', 'simple', 'mur']
    results = {}

    for abc_type in abc_types:
        fdtd = FDTD_1D(Nx=300, dx=1e-3, courant=0.99)
        fdtd.compute_coefficients()

        source_pos = 100
        Ey_history = []

        for n in range(400):
            fdtd.update_H()

            t = n * fdtd.dt
            source = gaussian_pulse(t, t0=5e-11, tau=2e-11)
            fdtd.Ey[source_pos] += source

            fdtd.update_E()

            if abc_type == 'pec':
                fdtd.Ey[0] = 0
                fdtd.Ey[-1] = 0
            elif abc_type == 'simple':
                fdtd.apply_abc_simple()
            else:
                fdtd.apply_abc_mur()

            if n % 4 == 0:
                Ey_history.append(fdtd.Ey.copy())

        results[abc_type] = np.array(Ey_history)

    # ์‹œ๊ฐํ™”
    x = np.arange(300) * 1e-3 * 1000

    fig, axes = plt.subplots(3, 3, figsize=(15, 12))

    for row, abc_type in enumerate(abc_types):
        Ey_history = results[abc_type]

        for col, snap_idx in enumerate([20, 50, 80]):
            ax = axes[row, col]
            ax.plot(x, Ey_history[snap_idx], 'b-', linewidth=1.5)
            ax.axvline(x=100, color='red', linestyle='--', alpha=0.5)
            ax.set_ylim(-1.2, 1.2)
            ax.grid(True, alpha=0.3)

            if row == 2:
                ax.set_xlabel('x [mm]')
            if col == 0:
                ax.set_ylabel(f'{abc_type.upper()}\nEy')

            ax.set_title(f'Step {snap_idx * 4}')

    plt.suptitle('ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด ๋น„๊ต: PEC vs Simple ABC vs Mur ABC', fontsize=14)
    plt.tight_layout()
    plt.savefig('fdtd_abc_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()

    # ๋ฐ˜์‚ฌ ๋ถ„์„
    fig, ax = plt.subplots(figsize=(10, 5))

    t = np.arange(len(results['pec'])) * 4 * 1e-3 / c0 * 1e9

    # ํŠน์ • ์œ„์น˜์—์„œ์˜ ํ•„๋“œ ๊ธฐ๋ก (๋ฐ˜์‚ฌ ๊ฒ€์ถœ์šฉ)
    probe_pos = 50

    for abc_type, color in [('pec', 'red'), ('simple', 'blue'), ('mur', 'green')]:
        field = results[abc_type][:, probe_pos]
        ax.plot(t, field, color=color, linewidth=1.5, label=abc_type.upper())

    ax.set_xlabel('t [ns]')
    ax.set_ylabel('Ey at probe')
    ax.set_title('๊ฒฝ๊ณ„์—์„œ์˜ ๋ฐ˜์‚ฌ ๋น„๊ต (probe at x = 50 mm)')
    ax.legend()
    ax.grid(True, alpha=0.3)

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

# abc_comparison()

4. 2D FDTD ๊ตฌํ˜„

4.1 TM ๋ชจ๋“œ (Ez, Hx, Hy)

class FDTD_2D_TM:
    """2D FDTD TM ๋ชจ๋“œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ"""

    def __init__(self, Nx=100, Ny=100, dx=1e-3, dy=1e-3, courant=0.99):
        self.Nx = Nx
        self.Ny = Ny
        self.dx = dx
        self.dy = dy

        # Courant ์กฐ๊ฑด
        self.dt = courant / (c0 * np.sqrt(1/dx**2 + 1/dy**2))

        # ํ•„๋“œ ๋ฐฐ์—ด
        self.Ez = np.zeros((Ny, Nx))
        self.Hx = np.zeros((Ny, Nx))
        self.Hy = np.zeros((Ny, Nx))

        # ๋ฌผ์„ฑ์น˜ ๋ฐฐ์—ด
        self.eps_r = np.ones((Ny, Nx))
        self.mu_r = np.ones((Ny, Nx))
        self.sigma = np.zeros((Ny, Nx))

        # ๊ณ„์ˆ˜
        self.Ca = None
        self.Cb = None

        self.time_step = 0

        print(f"2D FDTD TM ๋ชจ๋“œ ์ดˆ๊ธฐํ™”:")
        print(f"  ๊ฒฉ์ž: {Nx} x {Ny}")
        print(f"  dx = {dx*1000:.2f} mm, dy = {dy*1000:.2f} mm")
        print(f"  dt = {self.dt*1e12:.3f} ps")

    def set_material_region(self, x1, x2, y1, y2, eps_r=1, sigma=0):
        """์žฌ๋ฃŒ ์˜์—ญ ์„ค์ •"""
        self.eps_r[y1:y2, x1:x2] = eps_r
        self.sigma[y1:y2, x1:x2] = sigma

    def add_pec_circle(self, cx, cy, radius):
        """์›ํ˜• PEC ์ถ”๊ฐ€"""
        for j in range(self.Ny):
            for i in range(self.Nx):
                if (i - cx)**2 + (j - cy)**2 < radius**2:
                    self.sigma[j, i] = 1e7

    def compute_coefficients(self):
        """์—…๋ฐ์ดํŠธ ๊ณ„์ˆ˜ ๊ณ„์‚ฐ"""
        eps = eps0 * self.eps_r

        self.Ca = (1 - self.sigma * self.dt / (2 * eps)) / \
                  (1 + self.sigma * self.dt / (2 * eps))
        self.Cb = (self.dt / eps) / (1 + self.sigma * self.dt / (2 * eps))

    def update_H(self):
        """H ํ•„๋“œ ์—…๋ฐ์ดํŠธ"""
        # Hx ์—…๋ฐ์ดํŠธ: Hx = Hx - dt/ฮผโ‚€ * dEz/dy
        self.Hx[:, :-1] -= (self.dt / (mu0 * self.dy)) * \
                          (self.Ez[:, 1:] - self.Ez[:, :-1])

        # Hy ์—…๋ฐ์ดํŠธ: Hy = Hy + dt/ฮผโ‚€ * dEz/dx
        self.Hy[:-1, :] += (self.dt / (mu0 * self.dx)) * \
                          (self.Ez[1:, :] - self.Ez[:-1, :])

    def update_E(self):
        """E ํ•„๋“œ ์—…๋ฐ์ดํŠธ"""
        # Ez ์—…๋ฐ์ดํŠธ: Ez = Ca*Ez + Cb*(dHy/dx - dHx/dy)
        self.Ez[1:-1, 1:-1] = (
            self.Ca[1:-1, 1:-1] * self.Ez[1:-1, 1:-1] +
            self.Cb[1:-1, 1:-1] * (
                (self.Hy[1:-1, 1:-1] - self.Hy[:-2, 1:-1]) / self.dx -
                (self.Hx[1:-1, 1:-1] - self.Hx[1:-1, :-2]) / self.dy
            )
        )

    def apply_pec_boundary(self):
        """PEC ๊ฒฝ๊ณ„์กฐ๊ฑด"""
        self.Ez[0, :] = 0
        self.Ez[-1, :] = 0
        self.Ez[:, 0] = 0
        self.Ez[:, -1] = 0

    def add_point_source(self, x, y, value):
        """์  ์†Œ์Šค ์ถ”๊ฐ€"""
        self.Ez[y, x] += value

    def add_line_source(self, x, value):
        """์„  ์†Œ์Šค ์ถ”๊ฐ€ (y ๋ฐฉํ–ฅ ์ „์ฒด)"""
        self.Ez[:, x] += value

    def step(self, source_func=None, source_pos=None, source_type='point'):
        """ํ•œ ์‹œ๊ฐ„ ๋‹จ๊ณ„ ์ „์ง„"""
        self.update_H()

        if source_func is not None and source_pos is not None:
            t = self.time_step * self.dt
            value = source_func(t)

            if source_type == 'point':
                self.add_point_source(source_pos[0], source_pos[1], value)
            elif source_type == 'line':
                self.add_line_source(source_pos, value)

        self.update_E()
        self.apply_pec_boundary()

        self.time_step += 1

    def run(self, n_steps, source_func=None, source_pos=None,
           source_type='point', record_interval=1):
        """์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰"""
        self.compute_coefficients()

        Ez_history = []

        for n in range(n_steps):
            self.step(source_func, source_pos, source_type)

            if n % record_interval == 0:
                Ez_history.append(self.Ez.copy())

        return np.array(Ez_history)


def demo_2d_fdtd_tm():
    """2D FDTD TM ๋ชจ๋“œ ์‹œ์—ฐ"""

    # ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ƒ์„ฑ
    fdtd = FDTD_2D_TM(Nx=150, Ny=150, dx=1e-3, dy=1e-3, courant=0.9)

    # ์œ ์ „์ฒด ๋ธ”๋ก ์ถ”๊ฐ€
    fdtd.set_material_region(90, 120, 60, 90, eps_r=4)

    # PEC ์›ํ˜• ์‹ค๋ฆฐ๋” ์ถ”๊ฐ€
    fdtd.add_pec_circle(50, 75, 15)

    # ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰
    source_pos = (20, 75)  # (x, y)

    def source(t):
        return gaussian_pulse(t, t0=1e-10, tau=3e-11)

    Ez_history = fdtd.run(
        n_steps=300,
        source_func=source,
        source_pos=source_pos,
        source_type='point',
        record_interval=5
    )

    # ์‹œ๊ฐํ™”
    x = np.arange(fdtd.Nx) * fdtd.dx * 1000
    y = np.arange(fdtd.Ny) * fdtd.dy * 1000
    X, Y = np.meshgrid(x, y)

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

    snapshot_indices = [5, 15, 25, 35, 45, 55]

    for idx, snap_idx in enumerate(snapshot_indices):
        ax = axes[idx // 3, idx % 3]
        vmax = np.max(np.abs(Ez_history[snap_idx])) * 0.7
        if vmax == 0:
            vmax = 1

        im = ax.pcolormesh(X, Y, Ez_history[snap_idx], cmap='RdBu_r',
                          shading='auto', vmin=-vmax, vmax=vmax)

        # ์žฌ๋ฃŒ ๊ฒฝ๊ณ„ ํ‘œ์‹œ
        ax.contour(X, Y, fdtd.eps_r, levels=[2], colors='green', linewidths=2)
        ax.contour(X, Y, fdtd.sigma, levels=[1e6], colors='black', linewidths=2)

        # ์†Œ์Šค ์œ„์น˜
        ax.plot(source_pos[0] * fdtd.dx * 1000,
               source_pos[1] * fdtd.dy * 1000, 'r*', markersize=10)

        ax.set_xlabel('x [mm]')
        ax.set_ylabel('y [mm]')
        ax.set_title(f'Step {snap_idx * 5}')
        ax.set_aspect('equal')
        plt.colorbar(im, ax=ax, label='Ez')

    plt.suptitle('2D FDTD TM ๋ชจ๋“œ: ์‚ฐ๋ž€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜', fontsize=14)
    plt.tight_layout()
    plt.savefig('fdtd_2d_tm_scattering.png', dpi=150, bbox_inches='tight')
    plt.show()

    return fdtd, Ez_history

# fdtd, Ez_history = demo_2d_fdtd_tm()

4.2 TE ๋ชจ๋“œ (Hz, Ex, Ey)

class FDTD_2D_TE:
    """2D FDTD TE ๋ชจ๋“œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ"""

    def __init__(self, Nx=100, Ny=100, dx=1e-3, dy=1e-3, courant=0.99):
        self.Nx = Nx
        self.Ny = Ny
        self.dx = dx
        self.dy = dy
        self.dt = courant / (c0 * np.sqrt(1/dx**2 + 1/dy**2))

        # ํ•„๋“œ ๋ฐฐ์—ด
        self.Hz = np.zeros((Ny, Nx))
        self.Ex = np.zeros((Ny, Nx))
        self.Ey = np.zeros((Ny, Nx))

        # ๋ฌผ์„ฑ์น˜
        self.eps_r = np.ones((Ny, Nx))
        self.sigma = np.zeros((Ny, Nx))

        self.time_step = 0

    def compute_coefficients(self):
        eps = eps0 * self.eps_r
        self.Ca = (1 - self.sigma * self.dt / (2 * eps)) / \
                  (1 + self.sigma * self.dt / (2 * eps))
        self.Cb = (self.dt / eps) / (1 + self.sigma * self.dt / (2 * eps))

    def update_E(self):
        """E ํ•„๋“œ ์—…๋ฐ์ดํŠธ"""
        # Ex = Ca*Ex + Cb * dHz/dy
        self.Ex[1:-1, :] = (
            self.Ca[1:-1, :] * self.Ex[1:-1, :] +
            self.Cb[1:-1, :] * (self.Hz[1:, :] - self.Hz[:-2, :]) / (2 * self.dy)
        )

        # Ey = Ca*Ey - Cb * dHz/dx
        self.Ey[:, 1:-1] = (
            self.Ca[:, 1:-1] * self.Ey[:, 1:-1] -
            self.Cb[:, 1:-1] * (self.Hz[:, 2:] - self.Hz[:, :-2]) / (2 * self.dx)
        )

    def update_H(self):
        """H ํ•„๋“œ ์—…๋ฐ์ดํŠธ"""
        # Hz = Hz + dt/ฮผโ‚€ * (dEx/dy - dEy/dx)
        self.Hz[1:-1, 1:-1] += (self.dt / mu0) * (
            (self.Ex[2:, 1:-1] - self.Ex[:-2, 1:-1]) / (2 * self.dy) -
            (self.Ey[1:-1, 2:] - self.Ey[1:-1, :-2]) / (2 * self.dx)
        )

    def step(self, source_func=None, source_pos=None):
        self.update_E()

        if source_func is not None and source_pos is not None:
            t = self.time_step * self.dt
            self.Hz[source_pos[1], source_pos[0]] += source_func(t)

        self.update_H()

        # PEC ๊ฒฝ๊ณ„
        self.Ex[0, :] = self.Ex[-1, :] = 0
        self.Ey[:, 0] = self.Ey[:, -1] = 0

        self.time_step += 1

5. PML (Perfectly Matched Layer)

5.1 PML ๊ฐœ๋…

PML (Berenger, 1994):

ํ•ต์‹ฌ ์•„์ด๋””์–ด:
- ํŒŒ๋™์„ ํก์ˆ˜ํ•˜๋Š” ์ธ๊ณต ๋งค์งˆ์ธต
- ๋งค์งˆ ๊ฒฝ๊ณ„์—์„œ ๋ฐ˜์‚ฌ ์—†์Œ (์ž„ํ”ผ๋˜์Šค ์ •ํ•ฉ)
- ์ธต ๋‚ด๋ถ€์—์„œ ์ง€์ˆ˜์  ๊ฐ์‡ 

๊ตฌํ˜„ ๋ฐฉ์‹:
1. Split-field PML: ํ•„๋“œ๋ฅผ ๋‘ ์„ฑ๋ถ„์œผ๋กœ ๋ถ„ํ• 
2. UPML (Uniaxial PML): ์ด๋ฐฉ์„ฑ ๋งค์งˆ ํ‘œํ˜„
3. CPML (Convolutional PML): ์ปจ๋ณผ๋ฃจ์…˜ ํ˜•ํƒœ

PML ๋งค๊ฐœ๋ณ€์ˆ˜:
- ์ธต ๋‘๊ป˜: ๋ณดํ†ต 8-20 ์…€
- ๊ฐ์‡  ํ”„๋กœํŒŒ์ผ: ๋‹คํ•ญ์‹ ๋˜๋Š” ์ง€์ˆ˜ํ•จ์ˆ˜
- ์ตœ๋Œ€ ์ „๋„๋„: ์ตœ์ ๊ฐ’ ์กด์žฌ
class FDTD_2D_TM_PML:
    """2D FDTD TM ๋ชจ๋“œ with UPML"""

    def __init__(self, Nx=100, Ny=100, dx=1e-3, dy=1e-3, pml_layers=10, courant=0.9):
        self.Nx = Nx
        self.Ny = Ny
        self.dx = dx
        self.dy = dy
        self.pml_layers = pml_layers

        self.dt = courant / (c0 * np.sqrt(1/dx**2 + 1/dy**2))

        # ํ•„๋“œ ๋ฐฐ์—ด
        self.Ez = np.zeros((Ny, Nx))
        self.Hx = np.zeros((Ny, Nx))
        self.Hy = np.zeros((Ny, Nx))

        # PML ๋ณด์กฐ ํ•„๋“œ
        self.Psi_Ez_x = np.zeros((Ny, Nx))
        self.Psi_Ez_y = np.zeros((Ny, Nx))
        self.Psi_Hx_y = np.zeros((Ny, Nx))
        self.Psi_Hy_x = np.zeros((Ny, Nx))

        # ๋ฌผ์„ฑ์น˜
        self.eps_r = np.ones((Ny, Nx))
        self.sigma = np.zeros((Ny, Nx))

        # PML ๊ณ„์ˆ˜ ์ดˆ๊ธฐํ™”
        self._setup_pml()

        self.time_step = 0

        print(f"2D FDTD TM + PML ์ดˆ๊ธฐํ™”:")
        print(f"  ๊ฒฉ์ž: {Nx} x {Ny}, PML: {pml_layers} layers")

    def _pml_profile(self, d, d_max, sigma_max, order=3):
        """PML ์ „๋„๋„ ํ”„๋กœํŒŒ์ผ"""
        return sigma_max * (d / d_max) ** order

    def _setup_pml(self):
        """PML ๊ณ„์ˆ˜ ์„ค์ •"""
        # ์ตœ์  ์ „๋„๋„
        sigma_max = 0.8 * (order + 1) / (eta0 * self.dx) if hasattr(self, 'order') else \
                   0.8 * 4 / (eta0 * self.dx)
        order = 3

        # x ๋ฐฉํ–ฅ PML ๊ณ„์ˆ˜
        self.sigma_x = np.zeros(self.Nx)
        self.sigma_x_star = np.zeros(self.Nx)  # dual grid

        for i in range(self.pml_layers):
            # ์ขŒ์ธก PML
            d = self.pml_layers - i
            self.sigma_x[i] = self._pml_profile(d, self.pml_layers, sigma_max, order)
            self.sigma_x_star[i] = self._pml_profile(d - 0.5, self.pml_layers, sigma_max, order)

            # ์šฐ์ธก PML
            d = i + 1
            self.sigma_x[-(i+1)] = self._pml_profile(d, self.pml_layers, sigma_max, order)
            self.sigma_x_star[-(i+1)] = self._pml_profile(d - 0.5, self.pml_layers, sigma_max, order)

        # y ๋ฐฉํ–ฅ PML ๊ณ„์ˆ˜
        self.sigma_y = np.zeros(self.Ny)
        self.sigma_y_star = np.zeros(self.Ny)

        for j in range(self.pml_layers):
            # ํ•˜๋‹จ PML
            d = self.pml_layers - j
            self.sigma_y[j] = self._pml_profile(d, self.pml_layers, sigma_max, order)
            self.sigma_y_star[j] = self._pml_profile(d - 0.5, self.pml_layers, sigma_max, order)

            # ์ƒ๋‹จ PML
            d = j + 1
            self.sigma_y[-(j+1)] = self._pml_profile(d, self.pml_layers, sigma_max, order)
            self.sigma_y_star[-(j+1)] = self._pml_profile(d - 0.5, self.pml_layers, sigma_max, order)

        # ๊ณ„์ˆ˜ ๊ณ„์‚ฐ
        self.b_x = np.exp(-self.sigma_x * self.dt / eps0)
        self.c_x = (1 - self.b_x) / (self.sigma_x * self.dx + 1e-10)
        self.b_x_star = np.exp(-self.sigma_x_star * self.dt / eps0)
        self.c_x_star = (1 - self.b_x_star) / (self.sigma_x_star * self.dx + 1e-10)

        self.b_y = np.exp(-self.sigma_y * self.dt / eps0)
        self.c_y = (1 - self.b_y) / (self.sigma_y * self.dy + 1e-10)
        self.b_y_star = np.exp(-self.sigma_y_star * self.dt / eps0)
        self.c_y_star = (1 - self.b_y_star) / (self.sigma_y_star * self.dy + 1e-10)

    def update_H(self):
        """H ํ•„๋“œ ์—…๋ฐ์ดํŠธ (PML ํฌํ•จ)"""
        # Hx ์—…๋ฐ์ดํŠธ
        dEz_dy = (self.Ez[:, 1:] - self.Ez[:, :-1]) / self.dy
        self.Psi_Hx_y[:, :-1] = (self.b_y[:, np.newaxis] * self.Psi_Hx_y[:, :-1] +
                                self.c_y[:, np.newaxis] * dEz_dy)
        self.Hx[:, :-1] -= self.dt / mu0 * (dEz_dy + self.Psi_Hx_y[:, :-1])

        # Hy ์—…๋ฐ์ดํŠธ
        dEz_dx = (self.Ez[1:, :] - self.Ez[:-1, :]) / self.dx
        self.Psi_Hy_x[:-1, :] = (self.b_x[np.newaxis, :] * self.Psi_Hy_x[:-1, :] +
                                self.c_x[np.newaxis, :] * dEz_dx)
        self.Hy[:-1, :] += self.dt / mu0 * (dEz_dx + self.Psi_Hy_x[:-1, :])

    def update_E(self):
        """E ํ•„๋“œ ์—…๋ฐ์ดํŠธ (PML ํฌํ•จ)"""
        eps = eps0 * self.eps_r

        dHy_dx = (self.Hy[1:-1, 1:-1] - self.Hy[:-2, 1:-1]) / self.dx
        dHx_dy = (self.Hx[1:-1, 1:-1] - self.Hx[1:-1, :-2]) / self.dy

        self.Psi_Ez_x[1:-1, 1:-1] = (self.b_x_star[np.newaxis, 1:-1] * self.Psi_Ez_x[1:-1, 1:-1] +
                                    self.c_x_star[np.newaxis, 1:-1] * dHy_dx)
        self.Psi_Ez_y[1:-1, 1:-1] = (self.b_y_star[1:-1, np.newaxis] * self.Psi_Ez_y[1:-1, 1:-1] +
                                    self.c_y_star[1:-1, np.newaxis] * dHx_dy)

        self.Ez[1:-1, 1:-1] += self.dt / eps[1:-1, 1:-1] * (
            dHy_dx + self.Psi_Ez_x[1:-1, 1:-1] -
            dHx_dy - self.Psi_Ez_y[1:-1, 1:-1]
        )

    def step(self, source_func=None, source_pos=None):
        self.update_H()

        if source_func is not None and source_pos is not None:
            t = self.time_step * self.dt
            self.Ez[source_pos[1], source_pos[0]] += source_func(t)

        self.update_E()

        # PEC ์™ธ๋ถ€ ๊ฒฝ๊ณ„
        self.Ez[0, :] = self.Ez[-1, :] = 0
        self.Ez[:, 0] = self.Ez[:, -1] = 0

        self.time_step += 1

    def run(self, n_steps, source_func=None, source_pos=None, record_interval=1):
        Ez_history = []

        for n in range(n_steps):
            self.step(source_func, source_pos)

            if n % record_interval == 0:
                Ez_history.append(self.Ez.copy())

        return np.array(Ez_history)


def demo_pml():
    """PML ์‹œ์—ฐ"""

    # PML ์—†๋Š” ๊ฒฝ์šฐ
    fdtd_no_pml = FDTD_2D_TM(Nx=100, Ny=100, dx=1e-3, dy=1e-3, courant=0.9)
    fdtd_no_pml.compute_coefficients()

    # PML ์žˆ๋Š” ๊ฒฝ์šฐ
    fdtd_pml = FDTD_2D_TM_PML(Nx=100, Ny=100, dx=1e-3, dy=1e-3, pml_layers=10, courant=0.9)

    source_pos = (50, 50)

    def source(t):
        return gaussian_pulse(t, t0=0.15e-9, tau=0.05e-9)

    n_steps = 200

    # ์‹คํ–‰
    Ez_no_pml = fdtd_no_pml.run(n_steps, source, source_pos, record_interval=10)
    Ez_pml = fdtd_pml.run(n_steps, source, source_pos, record_interval=10)

    # ๋น„๊ต ์‹œ๊ฐํ™”
    fig, axes = plt.subplots(2, 4, figsize=(16, 8))

    x = np.arange(100) * 1e-3 * 1000
    y = np.arange(100) * 1e-3 * 1000
    X, Y = np.meshgrid(x, y)

    for col, snap_idx in enumerate([5, 10, 15, 19]):
        # PEC ๊ฒฝ๊ณ„ (๋ฐ˜์‚ฌ)
        ax = axes[0, col]
        vmax = 0.3
        ax.pcolormesh(X, Y, Ez_no_pml[snap_idx], cmap='RdBu_r',
                     shading='auto', vmin=-vmax, vmax=vmax)
        ax.set_title(f'PEC ๊ฒฝ๊ณ„, Step {snap_idx * 10}')
        ax.set_aspect('equal')
        if col == 0:
            ax.set_ylabel('Without PML\ny [mm]')

        # PML ๊ฒฝ๊ณ„ (ํก์ˆ˜)
        ax = axes[1, col]
        ax.pcolormesh(X, Y, Ez_pml[snap_idx], cmap='RdBu_r',
                     shading='auto', vmin=-vmax, vmax=vmax)

        # PML ์˜์—ญ ํ‘œ์‹œ
        pml = 10
        ax.axvline(x=pml, color='green', linestyle='--', alpha=0.5)
        ax.axvline(x=100-pml, color='green', linestyle='--', alpha=0.5)
        ax.axhline(y=pml, color='green', linestyle='--', alpha=0.5)
        ax.axhline(y=100-pml, color='green', linestyle='--', alpha=0.5)

        ax.set_title(f'PML ๊ฒฝ๊ณ„, Step {snap_idx * 10}')
        ax.set_aspect('equal')
        if col == 0:
            ax.set_ylabel('With PML\ny [mm]')
        ax.set_xlabel('x [mm]')

    plt.suptitle('PEC vs PML ๊ฒฝ๊ณ„์กฐ๊ฑด ๋น„๊ต', fontsize=14)
    plt.tight_layout()
    plt.savefig('fdtd_pml_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()

# demo_pml()

6. ์‘์šฉ ์˜ˆ์ œ: ๋„ํŒŒ๊ด€

6.1 ์ง์‚ฌ๊ฐํ˜• ๋„ํŒŒ๊ด€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

def rectangular_waveguide():
    """์ง์‚ฌ๊ฐํ˜• ๋„ํŒŒ๊ด€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜"""

    # ๋„ํŒŒ๊ด€ ์น˜์ˆ˜ (WR-90: a=22.86mm, b=10.16mm)
    # ๊ฐ„์†Œํ™”๋œ ์น˜์ˆ˜ ์‚ฌ์šฉ
    a = 30  # ๋„ํŒŒ๊ด€ ํญ (์…€ ์ˆ˜)
    b = 15  # ๋„ํŒŒ๊ด€ ๋†’์ด (์…€ ์ˆ˜)

    Nx = 150
    Ny = 40
    dx = dy = 1e-3  # 1 mm

    fdtd = FDTD_2D_TM(Nx=Nx, Ny=Ny, dx=dx, dy=dy, courant=0.9)

    # ๋„ํŒŒ๊ด€ ๋ฒฝ (PEC)
    wall_y1 = (Ny - b) // 2
    wall_y2 = wall_y1 + b

    # ์ƒ๋‹จ/ํ•˜๋‹จ PEC ๋ฒฝ
    fdtd.sigma[:wall_y1, :] = 1e7
    fdtd.sigma[wall_y2:, :] = 1e7

    fdtd.compute_coefficients()

    # TE10 ๋ชจ๋“œ ์—ฌ๊ธฐ ์ฃผํŒŒ์ˆ˜
    f_c = c0 / (2 * a * dx)  # ์ฐจ๋‹จ ์ฃผํŒŒ์ˆ˜
    f_op = 1.5 * f_c  # ์šด์šฉ ์ฃผํŒŒ์ˆ˜

    print(f"์ฐจ๋‹จ ์ฃผํŒŒ์ˆ˜: {f_c/1e9:.2f} GHz")
    print(f"์šด์šฉ ์ฃผํŒŒ์ˆ˜: {f_op/1e9:.2f} GHz")

    # ์†Œ์Šค (๋„ํŒŒ๊ด€ ๋‚ด๋ถ€)
    source_x = 10
    source_y = Ny // 2

    def source(t):
        t0 = 0.2e-9
        tau = 0.05e-9
        return np.sin(2 * np.pi * f_op * t) * (1 - np.exp(-((t - t0)/tau)**2) if t < t0 else 1)

    # ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    n_steps = 500
    Ez_history = []

    for n in range(n_steps):
        fdtd.step(source, (source_x, source_y))

        if n % 5 == 0:
            Ez_history.append(fdtd.Ez.copy())

    Ez_history = np.array(Ez_history)

    # ์‹œ๊ฐํ™”
    fig, axes = plt.subplots(2, 3, figsize=(15, 8))

    x = np.arange(Nx) * dx * 1000
    y = np.arange(Ny) * dy * 1000
    X, Y = np.meshgrid(x, y)

    for idx, snap_idx in enumerate([10, 30, 50, 70, 90]):
        ax = axes[idx // 3, idx % 3]
        vmax = np.max(np.abs(Ez_history[snap_idx])) * 0.5
        if vmax == 0:
            vmax = 1

        im = ax.pcolormesh(X, Y, Ez_history[snap_idx], cmap='RdBu_r',
                          shading='auto', vmin=-vmax, vmax=vmax)

        # ๋„ํŒŒ๊ด€ ๋ฒฝ ํ‘œ์‹œ
        ax.axhline(y=wall_y1 * dy * 1000, color='black', linewidth=2)
        ax.axhline(y=wall_y2 * dy * 1000, color='black', linewidth=2)

        ax.set_xlabel('x [mm]')
        ax.set_ylabel('y [mm]')
        ax.set_title(f'Step {snap_idx * 5}')
        plt.colorbar(im, ax=ax)

    # ํŒŒํ˜• ๋ถ„์„
    ax = axes[1, 2]
    probe_y = Ny // 2
    probe_x_list = [30, 60, 90, 120]

    for px in probe_x_list:
        signal = Ez_history[:, probe_y, px]
        t = np.arange(len(signal)) * 5 * fdtd.dt * 1e9
        ax.plot(t, signal, label=f'x={px*dx*1000:.0f}mm')

    ax.set_xlabel('t [ns]')
    ax.set_ylabel('Ez')
    ax.set_title('๋‹ค์–‘ํ•œ ์œ„์น˜์—์„œ์˜ ํ•„๋“œ')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.suptitle(f'์ง์‚ฌ๊ฐํ˜• ๋„ํŒŒ๊ด€ (f_op = {f_op/1e9:.1f} GHz > f_c = {f_c/1e9:.1f} GHz)', fontsize=14)
    plt.tight_layout()
    plt.savefig('fdtd_waveguide.png', dpi=150, bbox_inches='tight')
    plt.show()

# rectangular_waveguide()

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

์—ฐ์Šต 1: ์†Œ์Šค ๋น„๊ต

๊ฐ€์šฐ์‹œ์•ˆ ํŽ„์Šค์™€ Ricker wavelet์„ ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•  ๋•Œ 1D FDTD ๊ฒฐ๊ณผ๋ฅผ ๋น„๊ตํ•˜์‹œ์˜ค. ์ฃผํŒŒ์ˆ˜ ์‘๋‹ต ํŠน์„ฑ์„ ๋ถ„์„ํ•˜์‹œ์˜ค.

์—ฐ์Šต 2: ABC ์„ฑ๋Šฅ

1์ฐจ Mur ABC์™€ 2์ฐจ Mur ABC์˜ ๋ฐ˜์‚ฌ ๊ณ„์ˆ˜๋ฅผ ๋น„๊ตํ•˜์‹œ์˜ค. ์ž…์‚ฌ๊ฐ์— ๋”ฐ๋ฅธ ์„ฑ๋Šฅ์„ ๋ถ„์„ํ•˜์‹œ์˜ค.

์—ฐ์Šต 3: PML ์ตœ์ ํ™”

PML ์ธต ๋‘๊ป˜(5, 10, 15, 20)์™€ ๋‹คํ•ญ์‹ ์ฐจ์ˆ˜(2, 3, 4)์— ๋”ฐ๋ฅธ ํก์ˆ˜ ์„ฑ๋Šฅ์„ ๋น„๊ตํ•˜์‹œ์˜ค.

์—ฐ์Šต 4: ๋„ํŒŒ๊ด€ ๋ชจ๋“œ

TE10 ์ฐจ๋‹จ ์ฃผํŒŒ์ˆ˜ ์ดํ•˜์™€ ์ด์ƒ์—์„œ์˜ ๋„ํŒŒ๊ด€ ์ „ํŒŒ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ณ  ์ฐจ์ด๋ฅผ ๋ถ„์„ํ•˜์‹œ์˜ค.


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

ํ•ต์‹ฌ ๋…ผ๋ฌธ

  • Yee (1966) "Numerical Solution of Initial Boundary Value Problems..."
  • Mur (1981) "Absorbing Boundary Conditions for the Finite-Difference Approximation..."
  • Berenger (1994) "A Perfectly Matched Layer for the Absorption of Electromagnetic Waves"

๊ต์žฌ

  • Taflove & Hagness, "Computational Electrodynamics: The FDTD Method"
  • Sullivan, "Electromagnetic Simulation Using the FDTD Method"

์˜คํ”ˆ์†Œ์Šค

  • MEEP (MIT): Python/C++ FDTD
  • gprMax: Ground Penetrating Radar FDTD
  • OpenEMS: FDTD + ํšŒ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

์š”์•ฝ

FDTD ๊ตฌํ˜„ ํ•ต์‹ฌ:

1. ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌ์กฐ:
   - H ์—…๋ฐ์ดํŠธ โ†’ ์†Œ์Šค ์ฃผ์ž… โ†’ E ์—…๋ฐ์ดํŠธ โ†’ ABC

2. ์†Œ์Šค ์œ ํ˜•:
   - Hard: ๊ฐ•์ œ ์„ค์ • (๋ฐ˜์‚ฌ ๋ฐœ์ƒ)
   - Soft: ์ถ”๊ฐ€ (+= ) (๋ฐ˜์‚ฌํŒŒ ํ†ต๊ณผ)
   - TF/SF: ์ •ํ™•ํ•œ ํ‰๋ฉดํŒŒ

3. ํก์ˆ˜ ๊ฒฝ๊ณ„์กฐ๊ฑด:
   - Simple ABC: ๊ฐ€์žฅ ๋‹จ์ˆœ, S=1์—์„œ๋งŒ ์ •ํ™•
   - Mur ABC: ๊ฐœ์„ ๋œ ์„ฑ๋Šฅ, ์ˆ˜์ง ์ž…์‚ฌ์— ํšจ๊ณผ์ 
   - PML: ์ตœ๊ณ  ์„ฑ๋Šฅ, ๊ตฌํ˜„ ๋ณต์žก

4. 2D ๋ชจ๋“œ:
   - TM: Ez, Hx, Hy (z ํŽธ๊ด‘)
   - TE: Hz, Ex, Ey (z ํŽธ๊ด‘)

5. PML ์š”์†Œ:
   - ์ธต ๋‘๊ป˜: 8-20 ์…€
   - ๋‹คํ•ญ์‹ ํ”„๋กœํŒŒ์ผ (order 3-4)
   - CPML์ด ๊ฐ€์žฅ ํšจ๊ณผ์ 

6. ์ˆ˜์น˜์  ๊ณ ๋ ค:
   - Courant ์กฐ๊ฑด ์ค€์ˆ˜
   - ํŒŒ์žฅ๋‹น 10-20 ์…€
   - ์ˆ˜์น˜ ๋ถ„์‚ฐ ์ตœ์†Œํ™”

๋‹ค์Œ ๋ ˆ์Šจ์—์„œ๋Š” MHD (์ž๊ธฐ์œ ์ฒด์—ญํ•™)์˜ ๊ธฐ์ดˆ ์ด๋ก ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

to navigate between lessons