Introduction

Every radio signal, laser pulse, and WiFi packet obeys the same four laws: Maxwell's equations. Written down in 1865, they describe how electric and magnetic fields create and sustain each other as they ripple through space at the speed of light.

For simple geometries you can solve Maxwell's equations with pen and paper. For anything real — a chip antenna, a photonic crystal, a radar cross-section — you need a computer. The dominant numerical method is the Finite-Difference Time-Domain (FDTD) method, invented by Kane Yee in 1966.

The key insight is deceptively simple: don't solve for the field everywhere at once. Instead, leapfrog the electric and magnetic fields forward in time, each on a slightly offset grid. Electric and magnetic fields alternate in both space and time, and the update equations become a pair of explicit finite differences that any computer can crank through.

FDTD is the workhorse of computational electromagnetics. It is the reason your smartphone antenna fits inside a candy-bar shell.

Try It: Watch a Wave Diffract

The simulation below runs a minimal 2D FDTD grid. A sinusoidal source pulses at the left edge, and a barrier with a narrow slit sits in the middle. Watch how the wave diffracts — the plane wave bends around the slit and spreads outward in a circular arc, exactly as Huygens' principle predicts.

<!-- Simulation controls and canvas for the 2D FDTD single-slit diffraction demo -->
<div class="controls">
  <label>Slit width: <input id="slitSlider" type="range" min="2" max="10" value="4" step="1"> <span id="slitVal">4</span> cells</label>
  <button id="btnReset" type="button">Reset</button>
  <button id="btnToggle" type="button">Pause</button>
</div>
<canvas id="fdtd" width="320" height="280"></canvas>
<div id="status" class="status">Simulation running…</div>
<p class="hint">The wave (red = positive field, blue = negative) travels left-to-right and diffracts through the slit in the gray barrier. Try different slit widths: a narrower slit spreads the wave more.</p>
/* Styles for the FDTD single-slit diffraction demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; padding: .5rem; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 90px; }
button { font: 600 13px system-ui, sans-serif; padding: .35rem .75rem;
         border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button:hover { background: #274f78; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; width: 320px; height: 280px; }
.status { font-size: .85rem; font-weight: 600; margin-top: .35rem; color: #444; min-height: 1.2em; }
.hint { font-size: .8rem; color: #666; margin-top: .4rem; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2D FDTD grid dimensions and spatial step
const NX = 80, NY = 70;
const DX = 1.0, DY = 1.0;
// Courant-stable time step for 2D grid (must satisfy CFL condition) — CFL: dt = 0.5 / sqrt(1/dx^2 + 1/dy^2) ≈ 0.354
const DT = 0.5 / Math.sqrt(1 / (DX * DX) + 1 / (DY * DY));
const C = 1.0;

// Electromagnetic field arrays for Ez, Hx, Hy components
let Ez = new Float32Array(NX * NY);
let Hx = new Float32Array(NX * NY);
let Hy = new Float32Array(NX * NY);

const idx = (x, y) => x + y * NX;

// Barrier wall at WALL_X column — 1=wall, 0=open slit — barrier column and slit mask
let slitWidth = 4;
const WALL_X = Math.floor(NX * 0.45);
const wallMask = new Uint8Array(NY);

function buildWall(sw) {
  // Mark all cells as wall except the slit region around center
  const cy = Math.floor(NY / 2);
  const half = Math.floor(sw / 2);
  wallMask.fill(1);
  for (let y = cy - half; y <= cy + half; y++) {
    if (y >= 0 && y < NY) wallMask[y] = 0;
  }
}

// Sinusoidal source parameters at left edge
const SRC_X = 3;
const FREQ = 0.08;
let t = 0;
let running = true;
let stepCount = 0;

function resetSim(sw) {
  // Reset all fields to zero and rebuild the wall for the given slit width
  slitWidth = sw;
  Ez.fill(0); Hx.fill(0); Hy.fill(0);
  t = 0; stepCount = 0;
  buildWall(sw);
}

// Yee leapfrog H-field update using central differences — Yee leapfrog: H update at half-integer time steps
function updateH() {
  const coeff = DT / (DX);
  for (let y = 0; y < NY - 1; y++) {
    for (let x = 0; x < NX - 1; x++) {
      Hx[idx(x, y)] -= coeff * (Ez[idx(x, y + 1)] - Ez[idx(x, y)]);
      Hy[idx(x, y)] += coeff * (Ez[idx(x + 1, y)] - Ez[idx(x, y)]);
    }
  }
}

// Yee leapfrog E-field update using central differences — Yee leapfrog: E update at integer time steps
function updateE() {
  const coeff = DT / DX;
  for (let y = 1; y < NY - 1; y++) {
    for (let x = 1; x < NX - 1; x++) {
      // Perfectly conducting wall absorbs the field (set Ez to zero) — wall cells: perfectly conducting (Ez = 0)
      if (x === WALL_X && wallMask[y] === 1) {
        Ez[idx(x, y)] = 0;
        continue;
      }
      Ez[idx(x, y)] += coeff * (
        (Hy[idx(x, y)] - Hy[idx(x - 1, y)]) -
        (Hx[idx(x, y)] - Hx[idx(x, y - 1)])
      );
    }
  }
  // Inject sinusoidal plane-wave source at left boundary — soft sinusoidal source
  const val = Math.sin(2 * Math.PI * FREQ * t);
  for (let y = 1; y < NY - 1; y++) {
    Ez[idx(SRC_X, y)] += val * 0.5;
  }
  // Simple absorbing boundary condition at right wall — first-order absorbing boundary at right edge
  for (let y = 0; y < NY; y++) {
    Ez[idx(NX - 1, y)] = Ez[idx(NX - 2, y)];
  }
  t += DT;
}

// Draw the Ez field to the canvas with a diverging blue-white-red colormap
const canvas = document.getElementById('fdtd');
const ctx = canvas.getContext('2d');
const CW = canvas.width / NX;
const CH = canvas.height / NY;

function render() {
  const img = ctx.createImageData(canvas.width, canvas.height);
  const d = img.data;
  for (let y = 0; y < NY; y++) {
    for (let x = 0; x < NX; x++) {
      const v = Ez[idx(x, y)];
      // Map field amplitude to color — positive=red, zero=white, negative=blue — blue=negative, white=zero, red=positive
      const norm = Math.max(-1, Math.min(1, v * 2));
      const px = Math.floor(x * CW), py = Math.floor(y * CH);
      let r, g, b;
      if (norm >= 0) { r = 220; g = Math.round(220 - norm * 190); b = Math.round(220 - norm * 220); }
      else { r = Math.round(220 + norm * 220); g = Math.round(220 + norm * 190); b = 220; }
      // Render barrier wall cells as dark gray — draw wall as dark gray
      const isWall = (x === WALL_X && wallMask[y] === 1);
      if (isWall) { r = 60; g = 60; b = 60; }
      for (let dy = 0; dy < Math.ceil(CH); dy++) {
        for (let dx2 = 0; dx2 < Math.ceil(CW); dx2++) {
          const pi = ((py + dy) * canvas.width + (px + dx2)) * 4;
          if (pi + 3 >= d.length) continue;
          d[pi] = r; d[pi + 1] = g; d[pi + 2] = b; d[pi + 3] = 255;
        }
      }
    }
  }
  ctx.putImageData(img, 0, 0);
}

// Main animation loop — run 3 FDTD steps per frame for speed
let rafId = null;
function step() {
  if (!running) return;
  for (let i = 0; i < 3; i++) { updateH(); updateE(); stepCount++; }
  render();
  document.getElementById('status').textContent = i18nFmt('Step %n%', { n: stepCount });
  rafId = requestAnimationFrame(step);
}

// Wire up slider and button event listeners
const slider = document.getElementById('slitSlider');
const slitVal = document.getElementById('slitVal');
slider.addEventListener('input', () => {
  slitVal.textContent = slider.value;
});
document.getElementById('btnReset').addEventListener('click', () => {
  cancelAnimationFrame(rafId);
  resetSim(parseInt(slider.value, 10));
  render();
  document.getElementById('status').textContent = 'Simulation running…';
  if (running) rafId = requestAnimationFrame(step);
});
const toggleBtn = document.getElementById('btnToggle');
toggleBtn.addEventListener('click', () => {
  running = !running;
  toggleBtn.textContent = running ? 'Pause' : 'Resume';
  if (running) rafId = requestAnimationFrame(step);
});

// Initialize simulation and start animation
resetSim(slitWidth);
render();
rafId = requestAnimationFrame(step);
Result
Console

Notice that narrowing the slit spreads the diffracted beam wider, while a wider slit keeps it narrower. That tradeoff between spatial confinement and angular spread is the same physics behind quantum simulation: the uncertainty principle is diffraction in disguise.

The Real Complexity

FDTD's update loop looks simple. The cost hides in the constraints.

Stability — the Courant condition. The time step Δt\Delta t must satisfy

Δt≤1c1(Δx)2+1(Δy)2+1(Δz)2\Delta t \leq \frac{1}{c\sqrt{\frac{1}{(\Delta x)^2} + \frac{1}{(\Delta y)^2} + \frac{1}{(\Delta z)^2}}}

where cc is the speed of light. Violate this and the simulation explodes. In practice you use Δt≈0.99⋅Δtmax⁡\Delta t \approx 0.99 \cdot \Delta t_{\max}.

Resolution. You need at least 10–20 grid cells per wavelength to keep numerical dispersion small. At optical frequencies (wavelength ∼500 nm\sim 500\,\text{nm}) in a 1 μm31\,\mu\text{m}^3 volume that is already millions of cells. A 3D simulation with NN cells per side costs O(N3)O(N^3) memory and O(N4)O(N^4) time (three spatial dimensions plus time).

Absorbing boundaries. A finite grid needs walls that let waves out without reflection. The standard solution — the perfectly matched layer (PML) — adds a lossy border region that absorbs incoming waves at any angle and any frequency without spurious reflections.

Dispersive materials. Real materials slow down different frequencies by different amounts. Modeling glass or silicon accurately requires auxiliary differential equations coupled to the main FDTD loop.

Despite all this, FDTD remains the method of choice because its update loop is embarrassingly parallel: every cell depends only on its immediate neighbors, so it maps perfectly onto GPU grids and distributed-memory clusters. Modern solvers handle billions of cells.

Where It Matters

Every domain where electromagnetic waves interact with complex geometry relies on FDTD:

  • Antenna and RF design: every smartphone, laptop, and base-station antenna is shaped by FDTD. Engineers iterate the geometry in simulation instead of building dozens of prototypes.
  • Photonic integrated circuits: nanoscale waveguides, resonators, and modulators are designed with FDTD to route light on a chip the way copper routes current.
  • Radar cross-section: defense engineers use FDTD to predict how aircraft scatter radar waves and to design stealth surfaces.
  • MRI coil design: the radiofrequency fields inside an MRI scanner must be uniform and safe. FDTD models the coil together with a simulated human body.
  • Optical lithography: the wavelength of light is now smaller than the features being printed on chips. FDTD predicts diffraction effects so lithographers can pre-distort the mask.
  • Cancer hyperthermia: focused microwave energy can heat tumors. FDTD optimizes the applicator geometry and checks that surrounding tissue stays cool.

The thread running through all these is the same: a leapfrog on a staggered grid, looping until the physics settles. See also quantum simulation, where the same discretize-and-step philosophy solves the Schrödinger equation.

Conclusion

Kane Yee's 1966 paper fit on a few pages. The idea — alternate electric and magnetic field updates on a grid staggered in both space and time — is compact enough to write on a napkin and powerful enough to design the antenna in your pocket.

FDTD is a reminder that the right discretization can turn a continuous, infinite-dimensional problem into a finite, parallel, and numerically stable loop. The physics is exact in the limit of infinitely fine grids; the engineering is deciding how coarse you can afford to be before the errors matter.

The next time your phone finds a signal in a concrete building, or a surgeon targets a tumor with microwaves, a version of Yee's leapfrog ran somewhere in the design chain — quietly solving Maxwell's equations one half-step at a time.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/finite-difference-time-domain/Content licensed under CC BY-NC 4.0.