/** * FDTDSolver — 2D Finite-Difference Time-Domain wave equation solver. * Loaded as a plain script; class is defined on the global scope. */ /** * @typedef {Object} FDTDSolverConfig * @property {number} nx - grid width in cells * @property {number} ny - grid height in cells * @property {number} dx - spatial step size * @property {number} dt - time step size * @property {number} c - wave speed * @property {number} dampingWidth - absorbing boundary layer width in cells */ /** * @typedef {Object} BarrierConfig * @property {'single'|'double'} mode * @property {number} barrierCol - x-index of the barrier column * @property {number} slitWidth - slit width in grid cells * @property {number} slitSeparation - center-to-center distance (double-slit only) * @property {number} ny - grid height for centering */ class FDTDSolver { /** * @param {FDTDSolverConfig} config */ constructor(config) { this.config = config; this.nx = config.nx; this.ny = config.ny; this.dx = config.dx; this.dt = config.dt; this.c = config.c; this.dampingWidth = config.dampingWidth; const size = this.nx * this.ny; this.uPrev = new Float32Array(size); this.uCurr = new Float32Array(size); this.uNext = new Float32Array(size); this.barrier = new Uint8Array(size); this.time = 0; this.paused = false; } /** Zero all amplitude arrays and reset time counter. */ reset() { this.uPrev.fill(0); this.uCurr.fill(0); this.uNext.fill(0); this.time = 0; } /** * Advance n time steps using the FDTD update equation. * @param {number} n - number of sub-steps */ step(n) { if (this.paused) return; const { nx, ny, c, dt, dx, dampingWidth } = this; const C = c * dt / dx; const C2 = C * C; for (let s = 0; s < n; s++) { // 1. FDTD update for interior cells for (let y = 1; y < ny - 1; y++) { for (let x = 1; x < nx - 1; x++) { const idx = y * nx + x; const neighbors = this.uCurr[idx + 1] + // i+1, j this.uCurr[idx - 1] + // i-1, j this.uCurr[idx + nx] + // i, j+1 this.uCurr[idx - nx]; // i, j-1 this.uNext[idx] = 2.0 * this.uCurr[idx] - this.uPrev[idx] + C2 * (neighbors - 4.0 * this.uCurr[idx]); } } // 2. Enforce barrier cells to zero (Dirichlet boundary) const size = nx * ny; for (let i = 0; i < size; i++) { if (this.barrier[i] === 1) { this.uNext[i] = 0; } } // 3. Absorbing boundary damping on all four edges. // For columns near the source, skip top/bottom damping so the // plane wave source stays uniform and doesn't get eaten at the edges. const sourceCol = dampingWidth + 2; for (let y = 0; y < ny; y++) { for (let x = 0; x < nx; x++) { const dLeft = x; const dRight = nx - 1 - x; const dTop = y; const dBottom = ny - 1 - y; // For columns at or near the source, only damp left/right edges const nearSource = (x >= sourceCol - 1 && x <= sourceCol + 1); const dMin = nearSource ? Math.min(dLeft, dRight) : Math.min(dLeft, dRight, dTop, dBottom); if (dMin < dampingWidth) { const ratio = (dampingWidth - dMin) / dampingWidth; const damping = 1.0 - 0.99 * ratio * ratio * ratio; const idx = y * nx + x; this.uNext[idx] *= damping; this.uCurr[idx] *= damping; this.uPrev[idx] *= damping; } } } // 4. Rotate arrays: prev ← curr, curr ← next const temp = this.uPrev; this.uPrev = this.uCurr; this.uCurr = this.uNext; this.uNext = temp; // 5. Apply soft source injection if (this.wavelength) { this.applySource(this.time, this.wavelength); } this.time += this.dt; } } /** * Populate the barrier array from a BarrierConfig. * @param {BarrierConfig} config */ setBarrier(config) { const { mode, barrierCol, slitWidth, slitSeparation, ny } = config; const nx = this.nx; // Clear existing barrier this.barrier.fill(0); // Support custom slit center for per-slit barriers (orthogonal polarization) const centerY = config._slitCenter != null ? config._slitCenter : Math.floor(ny / 2); const halfSlit = Math.floor(slitWidth / 2); if (mode === 'single') { // Fill entire barrier column, then carve out the slit for (let y = 0; y < ny; y++) { this.barrier[y * nx + barrierCol] = 1; } // Open the slit: centered opening of slitWidth cells const slitStart = centerY - halfSlit; const slitEnd = slitStart + slitWidth; for (let y = slitStart; y < slitEnd; y++) { if (y >= 0 && y < ny) { this.barrier[y * nx + barrierCol] = 0; } } } else if (mode === 'double') { // Fill entire barrier column for (let y = 0; y < ny; y++) { this.barrier[y * nx + barrierCol] = 1; } // Two slits symmetric about center, separated by slitSeparation (center-to-center) const halfSep = Math.floor(slitSeparation / 2); const slit1Center = centerY - halfSep; const slit2Center = centerY + halfSep; // Open slit 1 const slit1Start = slit1Center - halfSlit; const slit1End = slit1Start + slitWidth; for (let y = slit1Start; y < slit1End; y++) { if (y >= 0 && y < ny) { this.barrier[y * nx + barrierCol] = 0; } } // Open slit 2 const slit2Start = slit2Center - halfSlit; const slit2End = slit2Start + slitWidth; for (let y = slit2Start; y < slit2End; y++) { if (y >= 0 && y < ny) { this.barrier[y * nx + barrierCol] = 0; } } } } /** * Soft additive source injection along the source column. * @param {number} time * @param {number} wavelength */ applySource(time, wavelength) { const { nx, ny, c, dx, dampingWidth } = this; const sourceCol = dampingWidth + 2; const A = this.sourceAmplitude != null ? this.sourceAmplitude : 1.0; const phase = 2.0 * Math.PI * time * c / (wavelength * dx); // Source width: how many cells of the column emit waves (centered) const sw = this.sourceWidth != null ? Math.min(this.sourceWidth, ny) : ny; const centerY = Math.floor(ny / 2); const halfSW = Math.floor(sw / 2); const yStart = Math.max(0, centerY - halfSW); const yEnd = Math.min(ny, centerY - halfSW + sw); // Hard source: force the amplitude at the source column. // This prevents backward-traveling waves from interfering with the // source and creating a standing wave pattern (point-source artifacts). const val = A * Math.sin(phase); for (let y = yStart; y < yEnd; y++) { this.uCurr[y * nx + sourceCol] = val; } } /** * Returns the current amplitude field. * @returns {Float32Array} */ getAmplitude() { return this.uCurr; } } window.FDTDSolver = FDTDSolver;