3D_slit_simulation / fdtd-solver.js
AK51's picture
Upload 8 files
a44e2de verified
/**
* 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;