Spaces:
Running
Running
| /** | |
| * 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; | |