import { add, sub, mul, scale, norm, complex, root, propagate, matrix, multiply, symmetricEigen, symmetricPower, solveComplex, type Complex } from './rcwa-math';
/**
* Lossless, isotropic, single binary layer; incidence perpendicular to the grooves.
* Reference: calculationsApp/python/lib/functions/{rcwa,calc_coherent_point,s_matrix}.py
* and SciApp/python/funkce.py. Uses their centered rectangular Fourier coefficients
* and inverse factorization for TM, reduced to scalar TE/TM eigenproblems.
* Coordinates here: periodic x, grooves y, depth +z; exp(-i omega t).
*/
export interface RCWASettings {
wavelength: number; period: number; thickness: number; fill: number; angle: number;
ridgeIndex: number; grooveIndex: number; incidentIndex: number; substrateIndex: number;
harmonics: number; polarization: 'TE' | 'TM';
}
export const DEFAULT_RCWA: RCWASettings = {
wavelength: 633, period: 700, thickness: 250, fill: .5, angle: 0,
ridgeIndex: 2, grooveIndex: 1, incidentIndex: 1, substrateIndex: 1.5,
harmonics: 7, polarization: 'TE',
};
export interface DiffractionOrder {
order: number; reflection: number; transmission: number;
reflectionAngle: number | null; transmissionAngle: number | null;
}
export interface RCWAResult {
settings: RCWASettings; reflection: number; transmission: number; balanceError: number;
orders: DiffractionOrder[]; warnings: string[];
modes: { w: number[][]; q: Complex[]; a: Complex[]; b: Complex[]; linear: boolean[] };
reflected: Complex[]; transmitted: Complex[]; kx: number[];
}
export interface FieldBounds { xMin: number; xMax: number; zMin: number; zMax: number }
export interface FieldGrid extends FieldBounds {
width: number; height: number; real: Float64Array; imaginary: Float64Array; peak: number;
beam?: { samples: number; radius: number; minAngle: number; maxAngle: number; warnings: string[] };
}
export interface SpectrumPoint { wavelength: number; reflection: number | null; transmission: number | null; error?: string }
function validate(s: RCWASettings) {
for (const key of ['wavelength', 'period', 'ridgeIndex', 'grooveIndex', 'incidentIndex', 'substrateIndex'] as const) {
if (!Number.isFinite(s[key]) || s[key] <= 0) throw new Error(`${key} must be positive and finite.`);
}
if (!Number.isFinite(s.thickness) || s.thickness < 0 || !Number.isFinite(s.fill) || s.fill < 0 || s.fill > 1 || !Number.isFinite(s.angle) || Math.abs(s.angle) >= 89) throw new Error('Invalid thickness, fill fraction, or incidence angle.');
if (!Number.isInteger(s.harmonics) || s.harmonics < 0 || s.harmonics > 20) throw new Error('Use 0–20 harmonics on each side of the zero order.');
if (s.polarization !== 'TE' && s.polarization !== 'TM') throw new Error('Choose TE or TM polarization.');
}
/** Analytic Fourier convolution for a ridge centered at x=0 in each period. */
export function convolution(n: number, fill: number, ridge: number, groove: number) {
if (fill === 0 || fill === 1 || ridge === groove) return matrix(n, (i, j) => i === j ? (fill === 0 ? groove : fill === 1 ? ridge : groove) : 0);
return matrix(n, (i, j) => i === j ? fill * ridge + (1 - fill) * groove :
(ridge - groove) * Math.sin(Math.PI * fill * (i - j)) / (Math.PI * (i - j)));
}
export function solveRCWA(settings: RCWASettings): RCWAResult {
validate(settings);
const s = { ...settings }, n = 2 * s.harmonics + 1, tm = s.polarization === 'TM';
const k0 = 2 * Math.PI / s.wavelength, length = k0 * s.thickness;
const kx = Array.from({ length: n }, (_, i) => s.incidentIndex * Math.sin(s.angle * Math.PI / 180) + (i - s.harmonics) * s.wavelength / s.period);
const eps = convolution(n, s.fill, s.ridgeIndex ** 2, s.grooveIndex ** 2);
const inverseEps = convolution(n, s.fill, 1 / s.ridgeIndex ** 2, 1 / s.grooveIndex ** 2);
let w: number[][], d: number[][], eigenvalues: number[];
if (tm) {
// (I - K E^-1 K) H = q² B H, B = convolution(1/epsilon).
// B^-1/2 makes the eigenproblem real symmetric; D = B W supplies admittance.
const epsInverse = symmetricPower(eps, -1), transform = symmetricPower(inverseEps, -.5);
const c = matrix(n, (i, j) => +(i === j) - kx[i] * epsInverse[i][j] * kx[j]);
const eig = symmetricEigen(multiply(multiply(transform, c), transform));
eigenvalues = eig.values; w = multiply(transform, eig.vectors); d = multiply(inverseEps, w);
} else {
const eig = symmetricEigen(matrix(n, (i, j) => eps[i][j] - (i === j ? kx[i] ** 2 : 0)));
eigenvalues = eig.values; w = eig.vectors; d = w;
}
const q = eigenvalues.map(root), linear = eigenvalues.map(value => Math.abs(value) < 1e-12);
const phase = q.map(value => propagate(value, length));
const outerQ = (index: number) => kx.map(value => root(index ** 2 - value ** 2));
const qr = outerQ(s.incidentIndex), qt = outerQ(s.substrateIndex);
const yr = qr.map(value => scale(value, tm ? 1 / s.incidentIndex ** 2 : 1));
const yt = qt.map(value => scale(value, tm ? 1 / s.substrateIndex ** 2 : 1));
const incidentFlux = yr[s.harmonics].re;
const system: Complex[][] = Array.from({ length: 2 * n }, () => Array.from({ length: 2 * n }, () => complex()));
const rhs = Array.from({ length: 2 * n }, (_, i) => complex(i === s.harmonics ? 2 * incidentFlux : 0));
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
const wi = complex(w[i][j]), v = scale(q[j], d[i][j]);
if (linear[j]) {
// At q=0 the two exponentials coincide. Use exact constant/linear solutions
// F=W(a+k0*z*b), G=-i D b instead of perturbing the wavelength or angle.
system[i][j] = mul(yr[i], wi);
system[i][n + j] = complex(0, -d[i][j]);
system[n + i][j] = scale(mul(yt[i], wi), -1);
system[n + i][n + j] = sub(complex(0, -d[i][j]), scale(mul(yt[i], wi), length));
} else {
// Modes are referenced to opposite faces, keeping all evanescent factors ≤1.
system[i][j] = add(v, mul(yr[i], wi));
system[i][n + j] = mul(sub(mul(yr[i], wi), v), phase[j]);
system[n + i][j] = mul(sub(v, mul(yt[i], wi)), phase[j]);
system[n + i][n + j] = scale(add(v, mul(yt[i], wi)), -1);
}
}
// An unexcited, uncoupled harmonic grazing in identical media has an arbitrary
// constant field: both interface equations only constrain its derivative.
// Select zero amplitude for that free solution, leaving the incident mode alone.
if (s.fill === 0 || s.fill === 1 || s.ridgeIndex === s.grooveIndex) {
for (let i = 0; i < n; i++) {
if (i !== s.harmonics && linear[i] && norm(qr[i]) < 1e-14 && norm(qt[i]) < 1e-14) {
system[i] = Array.from({ length: 2 * n }, (_, j) => complex(j === i ? 1 : 0));
}
}
}
const coefficients = solveComplex(system, rhs), a = coefficients.slice(0, n), b = coefficients.slice(n);
const reflected = Array.from({ length: n }, (_, i) => {
let value = complex(i === s.harmonics ? -1 : 0);
for (let j = 0; j < n; j++) value = add(value, scale(linear[j] ? a[j] : add(a[j], mul(phase[j], b[j])), w[i][j]));
return value;
});
const transmitted = Array.from({ length: n }, (_, i) => {
let value = complex();
for (let j = 0; j < n; j++) value = add(value, scale(linear[j] ? add(a[j], scale(b[j], length)) : add(mul(phase[j], a[j]), b[j]), w[i][j]));
return value;
});
const orders = kx.map((value, i) => ({
order: i - s.harmonics,
reflection: norm(reflected[i]) * yr[i].re / incidentFlux,
transmission: norm(transmitted[i]) * yt[i].re / incidentFlux,
reflectionAngle: qr[i].re > 0 ? Math.asin(value / s.incidentIndex) * 180 / Math.PI : null,
transmissionAngle: qt[i].re > 0 ? Math.asin(value / s.substrateIndex) * 180 / Math.PI : null,
}));
const reflection = orders.reduce((sum, order) => sum + order.reflection, 0);
const transmission = orders.reduce((sum, order) => sum + order.transmission, 0);
if (![reflection, transmission].every(Number.isFinite)) throw new Error('The calculation produced non-finite powers. Try a nearby setting.');
const balanceError = Math.abs(1 - reflection - transmission), warnings: string[] = [];
if (balanceError > 1e-6) warnings.push('Power balance is inaccurate at this setting. Treat the result as unreliable.');
const omitted = [-s.harmonics - 1, s.harmonics + 1].some(m => Math.abs(s.incidentIndex * Math.sin(s.angle * Math.PI / 180) + m * s.wavelength / s.period) < Math.max(s.incidentIndex, s.substrateIndex));
if (omitted) warnings.push('Some propagating orders are outside the retained harmonics. Increase the harmonic count.');
if ([...qr, ...qt].some(value => norm(value) < 1e-14)) warnings.push('An exterior order is at grazing cutoff. It carries zero normal power at the threshold.');
return { settings: s, reflection, transmission, balanceError, orders, warnings, modes: { w, q, a, b, linear }, reflected, transmitted, kx };
}
/** Normalized transverse scalar field: Ey for TE, Hy for TM (not total |E|²). */
export function sampleField(result: RCWAResult, width = 192, height = 128, bounds?: FieldBounds): FieldGrid {
const s = result.settings, { w, q, a, b, linear } = result.modes, n = result.kx.length;
const k0 = 2 * Math.PI / s.wavelength;
const { xMin, xMax, zMin, zMax } = bounds ?? { xMin: -1.5 * s.period, xMax: 1.5 * s.period, zMin: -.8 * s.wavelength, zMax: s.thickness + .8 * s.wavelength };
const real = new Float64Array(width * height), imaginary = new Float64Array(width * height);
const lateral = Array.from({ length: width }, (_, x) => result.kx.map(k => propagate(complex(k), k0 * (xMin + (xMax - xMin) * x / (width - 1)))));
let peak = 0;
for (let row = 0; row < height; row++) {
const z = zMin + (zMax - zMin) * row / (height - 1);
const field = Array.from({ length: n }, () => complex());
if (z < 0 || z > s.thickness) {
for (let i = 0; i < n; i++) {
const outer = root((z < 0 ? s.incidentIndex : s.substrateIndex) ** 2 - result.kx[i] ** 2);
field[i] = mul(z < 0 ? result.reflected[i] : result.transmitted[i], propagate(outer, k0 * (z < 0 ? -z : z - s.thickness)));
if (z < 0 && i === s.harmonics) field[i] = add(field[i], propagate(outer, k0 * z));
}
} else {
for (let j = 0; j < n; j++) {
const modal = linear[j] ? add(a[j], scale(b[j], k0 * z)) : add(mul(a[j], propagate(q[j], k0 * z)), mul(b[j], propagate(q[j], k0 * (s.thickness - z))));
for (let i = 0; i < n; i++) field[i] = add(field[i], scale(modal, w[i][j]));
}
}
for (let x = 0; x < width; x++) {
let value = complex();
for (let i = 0; i < n; i++) value = add(value, mul(field[i], lateral[x][i]));
real[row * width + x] = value.re; imaginary[row * width + x] = value.im;
peak = Math.max(peak, Math.sqrt(norm(value)));
}
}
return { width, height, real, imaginary, peak, xMin, xMax, zMin, zMax };
}