import { zeros, eye, solve, times, plus, put, block, slab, type Dense } from './rcwa-2d-math';
export interface RCWA2DSettings {
wavelength: number; periodX: number; periodY: number; thickness: number; fillX: number; fillY: number;
pillarIndex: number; gapIndex: number; incidentIndex: number; substrateIndex: number;
theta: number; phi: number; polarization: 's' | 'p' | 'diagonal' | 'circular'; harmonics: number;
}
export const DEFAULT_RCWA_2D: RCWA2DSettings = {
wavelength: 633, periodX: 700, periodY: 700, thickness: 220, fillX: .5, fillY: .5,
pillarIndex: 2, gapIndex: 1, incidentIndex: 1, substrateIndex: 1.5,
theta: 0, phi: 0, polarization: 's', harmonics: 1,
};
export interface Order2D {
m: number; n: number; kx: number; ky: number; reflection: number; transmission: number;
reflectedS: number; reflectedP: number; transmittedS: number; transmittedP: number;
thetaR: number | null; thetaT: number | null; azimuth: number;
}
export interface RCWA2DResult {
settings: RCWA2DSettings; reflection: number; transmission: number; balanceError: number; orders: Order2D[]; warnings: string[];
reflected: Dense; transmitted: Dense; incident: Dense;
topForward: Dense; bottomBackward: Dense;
}
const rectCoefficient = (order: number, fill: number) => order === 0 ? fill : fill === 0 || fill === 1 ? 0 : Math.sin(Math.PI * order * fill) / (Math.PI * order);
/** Centered rectangular pillars, using direct (Laurent) Fourier products. */
export function geometry2D(s: RCWA2DSettings) {
const orders = Array.from({ length: (2 * s.harmonics + 1) ** 2 }, (_, i) => ({ m: i % (2 * s.harmonics + 1) - s.harmonics, n: Math.floor(i / (2 * s.harmonics + 1)) - s.harmonics }));
const count = orders.length, eps = zeros(count), angle = s.theta * Math.PI / 180, phi = s.phi * Math.PI / 180;
const kx = orders.map(o => s.incidentIndex * Math.sin(angle) * Math.cos(phi) + o.m * s.wavelength / s.periodX);
const ky = orders.map(o => s.incidentIndex * Math.sin(angle) * Math.sin(phi) + o.n * s.wavelength / s.periodY);
for (let i = 0; i < count; i++) for (let j = 0; j < count; j++) eps.re[i * count + j] = (i === j ? s.gapIndex ** 2 : 0) + (s.pillarIndex ** 2 - s.gapIndex ** 2) * rectCoefficient(orders[i].m - orders[j].m, s.fillX) * rectCoefficient(orders[i].n - orders[j].n, s.fillY);
const inv = solve(eps, eye(count)), g = zeros(4 * count);
// d[Ex,Ey,Hx,Hy]/d(k0 z) = i G [Ex,Ey,Hx,Hy], exp(-i omega t).
const set = (r: number, c: number, i: number, j: number, v: number) => { g.re[(r * count + i) * g.cols + c * count + j] = v; };
for (let i = 0; i < count; i++) for (let j = 0; j < count; j++) {
const d = +(i === j), e = eps.re[i * count + j], v = inv.re[i * count + j];
set(0, 2, i, j, kx[i] * v * ky[j]); set(0, 3, i, j, d - kx[i] * v * kx[j]);
set(1, 2, i, j, ky[i] * v * ky[j] - d); set(1, 3, i, j, -ky[i] * v * kx[j]);
set(2, 0, i, j, -d * kx[i] * ky[i]); set(2, 1, i, j, d * kx[i] ** 2 - e);
set(3, 0, i, j, e - d * ky[i] ** 2); set(3, 1, i, j, d * ky[i] * kx[i]);
}
return { orders, kx, ky, g, inversePermittivity: inv };
}
/** Physical s/p amplitudes: propagating modes have unit electric-vector magnitude. */
function exterior(kx: number[], ky: number[], index: number, direction: 1 | -1, phi: number) {
const n = kx.length, e = zeros(2 * n), h = zeros(2 * n), fullE = zeros(3 * n, 2 * n), qReal: number[] = [];
for (let i = 0; i < n; i++) {
const rho = Math.hypot(kx[i], ky[i]), q2 = index ** 2 - rho ** 2;
if (Math.abs(q2) < 1e-11) throw new Error('A diffraction order is exactly at grazing cutoff. Shift the wavelength or angle slightly.');
const qr = q2 > 0 ? Math.sqrt(q2) : 0, qi = q2 < 0 ? Math.sqrt(-q2) : 0;
const ux = rho > 1e-12 ? kx[i] / rho : Math.cos(phi), uy = rho > 1e-12 ? ky[i] / rho : Math.sin(phi);
const set = (a: Dense, r: number, c: number, re: number, im = 0) => { a.re[r * a.cols + c] = re; a.im[r * a.cols + c] = im; };
set(e, i, i, -uy); set(e, n + i, i, ux);
set(e, i, n + i, direction * qr * ux / index, direction * qi * ux / index);
set(e, n + i, n + i, direction * qr * uy / index, direction * qi * uy / index);
set(h, i, i, -direction * qr * ux, -direction * qi * ux);
set(h, n + i, i, -direction * qr * uy, -direction * qi * uy);
set(h, i, n + i, -index * uy); set(h, n + i, n + i, index * ux);
qReal.push(qr);
for (let r = 0; r < 2 * n; r++) for (const c of [i, n + i]) set(fullE, r, c, e.re[r * e.cols + c], e.im[r * e.cols + c]);
set(fullE, 2 * n + i, n + i, -rho / index);
}
const a = zeros(2 * n), b = zeros(2 * n);
for (let i = 0; i < 2 * n; i++) for (let j = 0; j < 2 * n; j++) {
const other = i < n ? i + n : i - n, sign = i < n ? -1 : 1, pos = i * 2 * n + j, hp = other * 2 * n + j;
a.re[pos] = (e.re[pos] - sign * h.re[hp]) / 2; a.im[pos] = (e.im[pos] - sign * h.im[hp]) / 2;
b.re[pos] = (e.re[pos] + sign * h.re[hp]) / 2; b.im[pos] = (e.im[pos] + sign * h.im[hp]) / 2;
}
return { a, b, fullE, qReal };
}
export function solveRCWA2D(settings: RCWA2DSettings): RCWA2DResult {
const s = { ...settings };
for (const key of ['wavelength', 'periodX', 'periodY', 'pillarIndex', 'gapIndex', 'incidentIndex', 'substrateIndex'] as const) if (!Number.isFinite(s[key]) || s[key] <= 0) throw new Error(`${key} must be positive and finite.`);
if (!Number.isInteger(s.harmonics) || s.harmonics < 0 || s.harmonics > 3) throw new Error('Use 0–3 harmonics per axis.');
if (![s.fillX, s.fillY].every(v => Number.isFinite(v) && v >= 0 && v <= 1) || !Number.isFinite(s.thickness) || s.thickness < 0 || !Number.isFinite(s.theta) || Math.abs(s.theta) >= 85 || !Number.isFinite(s.phi)) throw new Error('Invalid geometry or incidence angle.');
if (!['s', 'p', 'diagonal', 'circular'].includes(s.polarization)) throw new Error('Invalid incident polarization.');
const { orders: indices, kx, ky, g } = geometry2D(s), count = indices.length, n = 2 * count;
const phi = s.phi * Math.PI / 180;
const above = exterior(kx, ky, s.incidentIndex, 1, phi), reflectedBasis = exterior(kx, ky, s.incidentIndex, -1, phi), below = exterior(kx, ky, s.substrateIndex, 1, phi);
const source = zeros(n, 1), zero = Math.floor(count / 2);
source.re[zero] = s.polarization === 's' ? 1 : s.polarization === 'p' ? 0 : Math.SQRT1_2;
source.re[count + zero] = s.polarization === 'p' ? 1 : s.polarization === 'diagonal' ? Math.SQRT1_2 : 0;
source.im[count + zero] = s.polarization === 'circular' ? Math.SQRT1_2 : 0;
const layer = slab(g, 2 * Math.PI * s.thickness / s.wavelength);
const aIn = times(above.a, source), bIn = times(above.b, source), system = zeros(2 * n), rhs = zeros(2 * n, 1);
put(system, plus(reflectedBasis.b, times(layer.r, reflectedBasis.a), -1), 0, 0);
put(system, times(layer.tBack, below.b), 0, n, -1);
put(system, times(layer.t, reflectedBasis.a), n, 0, -1);
put(system, plus(below.a, times(layer.rBack, below.b), -1), n, n);
put(rhs, plus(times(layer.r, aIn), bIn, -1), 0, 0); put(rhs, times(layer.t, aIn), n, 0);
const amplitudes = solve(system, rhs), r = block(amplitudes, 0, 0, n, 1), t = block(amplitudes, n, 0, n, 1);
const flux = s.incidentIndex * Math.cos(s.theta * Math.PI / 180);
const power = (a: Dense, i: number, q: number) => (a.re[i] ** 2 + a.im[i] ** 2) * q / flux;
const orders = indices.map((o, i): Order2D => {
const reflectedS = power(r, i, reflectedBasis.qReal[i]), reflectedP = power(r, count + i, reflectedBasis.qReal[i]);
const transmittedS = power(t, i, below.qReal[i]), transmittedP = power(t, count + i, below.qReal[i]);
return { ...o, kx: kx[i], ky: ky[i], reflectedS, reflectedP, transmittedS, transmittedP, reflection: reflectedS + reflectedP, transmission: transmittedS + transmittedP,
thetaR: reflectedBasis.qReal[i] > 0 ? Math.asin(Math.hypot(kx[i], ky[i]) / s.incidentIndex) * 180 / Math.PI : null,
thetaT: below.qReal[i] > 0 ? Math.asin(Math.hypot(kx[i], ky[i]) / s.substrateIndex) * 180 / Math.PI : null,
azimuth: Math.atan2(ky[i], kx[i]) * 180 / Math.PI };
});
const reflection = orders.reduce((sum, o) => sum + o.reflection, 0), transmission = orders.reduce((sum, o) => sum + o.transmission, 0), balanceError = Math.abs(1 - reflection - transmission);
if (![reflection, transmission].every(Number.isFinite)) throw new Error('The calculation produced non-finite power.');
const warnings: string[] = [];
if (balanceError > 1e-6) warnings.push('The power-balance check failed. Treat this result as unreliable.');
const maxIndex = Math.max(s.incidentIndex, s.substrateIndex), baseX = kx[zero], baseY = ky[zero];
for (let m = -s.harmonics - 1; m <= s.harmonics + 1; m++) for (let n = -s.harmonics - 1; n <= s.harmonics + 1; n++) {
if (Math.abs(m) <= s.harmonics && Math.abs(n) <= s.harmonics) continue;
if (Math.hypot(baseX + m * s.wavelength / s.periodX, baseY + n * s.wavelength / s.periodY) < maxIndex && !warnings.some(w => w.includes('omitted'))) warnings.push('Some propagating orders are omitted. Increase the harmonic count before interpreting total powers.');
}
return { settings: s, reflection, transmission, balanceError, orders, warnings,
topForward: plus(aIn, times(reflectedBasis.a, r)), bottomBackward: times(below.b, t),
reflected: times(reflectedBasis.fullE, r), transmitted: times(below.fullE, t), incident: times(above.fullE, source) };
}
export interface Field2D { width: number; height: number; real: Float64Array[]; imaginary: Float64Array[]; peak: number; intensityPeak: number }
/** Full electric vector on an x-y plane outside the patterned layer, over 3×3 cells. */
export function sampleField2D(result: RCWA2DResult, side: 'above' | 'below', distance: number, width = 128, height = 128): Field2D {
const s = result.settings, count = result.orders.length, k0 = 2 * Math.PI / s.wavelength;
const real = [0, 1, 2].map(() => new Float64Array(width * height)), imaginary = [0, 1, 2].map(() => new Float64Array(width * height));
const field = side === 'above' ? result.reflected : result.transmitted, index = side === 'above' ? s.incidentIndex : s.substrateIndex;
for (let i = 0; i < count; i++) {
const order = result.orders[i], q2 = index ** 2 - order.kx ** 2 - order.ky ** 2;
const q = Math.sqrt(Math.abs(q2)), damping = q2 < 0 ? Math.exp(-q * k0 * distance) : 1, phaseZ = q2 > 0 ? q * k0 * distance : 0;
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
const pos = y * width + x;
const xy = k0 * (order.kx * s.periodX * (3 * x / (width - 1) - 1.5) + order.ky * s.periodY * (3 * y / (height - 1) - 1.5));
const cr = damping * Math.cos(xy + phaseZ), ci = damping * Math.sin(xy + phaseZ);
for (let c = 0; c < 3; c++) {
const fr = field.re[c * count + i], fi = field.im[c * count + i];
real[c][pos] += fr * cr - fi * ci; imaginary[c][pos] += fr * ci + fi * cr;
if (side === 'above' && order.m === 0 && order.n === 0) {
const ir = result.incident.re[c * count + i], ii = result.incident.im[c * count + i];
real[c][pos] += ir * Math.cos(xy - phaseZ) - ii * Math.sin(xy - phaseZ);
imaginary[c][pos] += ir * Math.sin(xy - phaseZ) + ii * Math.cos(xy - phaseZ);
}
}
}
}
let peak = 0, intensityPeak = 0;
for (let i = 0; i < width * height; i++) {
let intensity = 0;
for (let c = 0; c < 3; c++) { const v = real[c][i] ** 2 + imaginary[c][i] ** 2; intensity += v; peak = Math.max(peak, Math.sqrt(v)); }
intensityPeak = Math.max(intensityPeak, intensity);
}
return { width, height, real, imaginary, peak, intensityPeak };
}