import { type RCWASettings } from './rcwa';
import { thinLayerSpecs } from './rcwa-incoherent-structure';
import { materialPermittivity, type Material } from './rcwa-materials';
import { admittances, interfaceScattering, dispersiveStack, diagonal, complexRoot } from './rcwa-dispersive';
import { complex, sub, propagate, norm } from './rcwa-math';
import { zeros, eye, solve, times, plus, block, join, type Dense, type Scattering } from './rcwa-2d-math';
/** Scalar, isotropic specialization of Ciompa (2018), §4.3, and
* SciApp/python/funkce.py:calculate_incoherent_partial_waves.
* All dimensions in nm. Each thick-layer traversal removes unequal-kz order coherence by default.
* Thin grating scattering still includes evanescent harmonics and interference. */
export interface IncoherentSettings extends RCWASettings {
spacerIndex: number; spacerThickness: number; thicknessSpread: number;
placement: 'substrate' | 'cap'; averageSamples: number;
ridgeMaterial: Material; gapMaterial: Material; spacerMaterial: Material; coatingMaterial: Material; coatingThickness: number; preserveSymmetry: boolean;
}
export const DEFAULT_INCOHERENT: IncoherentSettings = {
wavelength: 633, period: 1000000 / 740, thickness: 530, fill: .5, angle: 6,
ridgeIndex: 1.5, grooveIndex: 1, incidentIndex: 1, substrateIndex: 1,
harmonics: 5, polarization: 'TE', spacerIndex: 1.5, spacerThickness: 50000,
thicknessSpread: 100, placement: 'substrate', averageSamples: 41,
ridgeMaterial: 'CCL_grid_substrate', gapMaterial: 'air', spacerMaterial: 'CCL_grid_substrate', coatingMaterial: 'CCL_grid_coating', coatingThickness: 0, preserveSymmetry: true,
};
export interface Powers { reflection: number; transmission: number; r: number[]; t: number[]; absorption: number }
export interface IncoherentResult {
settings: IncoherentSettings; coherent: Powers; incoherent: Powers; averaged: Powers;
orders: { order: number; angleR: number | null; angleT: number | null; angleInside: number | null }[];
passes: { reflection: number; transmission: number; remaining: number }[];
warnings: string[];
}
const value = (a: Dense, i: number, j: number) => complex(a.re[i * a.cols + j], a.im[i * a.cols + j]);
const column = (a: Dense, col: number) => block(a, 0, col, a.rows, 1);
const sum = (a: number[]) => a.reduce((x, y) => x + y, 0);
function powers(r: number[], t: number[]): Powers {
const reflection = sum(r), transmission = sum(t);
if (![...r, ...t].every(Number.isFinite)) throw new Error('The optical calculation produced non-finite powers.');
return { r, t, reflection, transmission, absorption: 1 - reflection - transmission };
}
function powerMatrix(a: Dense, input: number[], output: number[]) {
const p = zeros(a.rows, a.cols);
for (let i = 0; i < a.rows; i++) for (let j = 0; j < a.cols; j++) p.re[i * a.cols + j] = input[j] > 0 ? norm(value(a, i, j)) * output[i] / input[j] : 0;
return p;
}
function intensityS(a: Scattering, left: number[], right: number[]): Scattering {
return { r: powerMatrix(a.r, left, left), t: powerMatrix(a.t, left, right),
rBack: powerMatrix(a.rBack, right, right), tBack: powerMatrix(a.tBack, right, left) };
}
export function solveIncoherent(input: IncoherentSettings): IncoherentResult {
const ridge = materialPermittivity(input.ridgeMaterial, input.wavelength), gap = materialPermittivity(input.gapMaterial, input.wavelength);
const spacer = materialPermittivity(input.spacerMaterial, input.wavelength), coating = input.coatingThickness > 0 ? materialPermittivity(input.coatingMaterial, input.wavelength) : complex(1);
// Original specimen substrates have E2=0 over the exposed spectral range.
if (spacer.im !== 0) throw new Error('Choose one of the transparent substrate spectra for the thick layer. Absorbing spectra are supported in the thin grating and coating.');
const s = { ...input, ridgeIndex: complexRoot(ridge).re, grooveIndex: complexRoot(gap).re, spacerIndex: complexRoot(spacer).re };
if (!Number.isInteger(s.harmonics) || s.harmonics < 0 || s.harmonics > 15 || !Number.isFinite(s.angle) || Math.abs(s.angle) >= 80 || !Number.isFinite(s.period) || s.period <= 0 || !Number.isFinite(s.fill) || s.fill < 0 || s.fill > 1 || !Number.isFinite(s.thickness) || s.thickness < 0 || !Number.isFinite(s.coatingThickness) || s.coatingThickness < 0 || s.coatingThickness > s.thickness || ![s.incidentIndex, s.substrateIndex].every(v => Number.isFinite(v) && v > 0)) throw new Error('Invalid geometry, angle, exterior index, or Fourier harmonics.');
if (!['substrate', 'cap'].includes(s.placement)) throw new Error('Choose a substrate or capping layer.');
if (![s.spacerIndex, s.spacerThickness].every(v => Number.isFinite(v) && v > 0) || !Number.isFinite(s.thicknessSpread) || s.thicknessSpread < 0 || 3 * s.thicknessSpread >= s.spacerThickness) throw new Error('The layer thickness must exceed three times its thickness spread; indices must be positive.');
if (!Number.isInteger(s.averageSamples) || s.averageSamples < 3 || s.averageSamples > 201) throw new Error('Use 3–201 thickness samples.');
const n = 2 * s.harmonics + 1, center = s.harmonics, tm = s.polarization === 'TM';
const lateral = s.incidentIndex * Math.sin(s.angle * Math.PI / 180);
if (Math.abs(lateral) >= s.spacerIndex) throw new Error('The central wave must propagate in the thick layer.');
const kx = Array.from({ length: n }, (_, i) => lateral + (i - center) * s.wavelength / s.period);
if ([s.incidentIndex, s.spacerIndex, s.substrateIndex].some(index => kx.some(k => Math.abs(index ** 2 - k ** 2) < 1e-10))) throw new Error('An order is exactly at grazing cutoff. Move the wavelength or angle slightly.');
const left = complex(s.incidentIndex ** 2), right = complex(s.substrateIndex ** 2);
const yl = admittances(kx, left, tm), ym = admittances(kx, spacer, tm), yr = admittances(kx, right, tm);
const layers = thinLayerSpecs(s).map(layer => ({ thickness: layer.thickness, fill: layer.fill,
ridge: materialPermittivity(layer.ridge, s.wavelength), gap: materialPermittivity(layer.gap, s.wavelength) }));
const g = dispersiveStack(kx, s.wavelength, tm, s.placement === 'cap' ? spacer : left, s.placement === 'cap' ? right : spacer, layers);
const a = s.placement === 'cap' ? interfaceScattering(yl, ym) : g;
const b = s.placement === 'cap' ? g : interfaceScattering(ym, yr);
const fl = yl.map(y => y.re), fm = ym.map(y => y.re), fr = yr.map(y => y.re);
const fromS = (total: Scattering) => powers(Array.from(column(powerMatrix(total.r, fl, fl), center).re), Array.from(column(powerMatrix(total.t, fl, fr), center).re));
const coherentAt = (thickness: number) => {
const p = diagonal(kx.map(k => propagate(complexRoot(complex(s.spacerIndex ** 2 - k ** 2)), 2 * Math.PI * thickness / s.wavelength)));
return fromS(join(join(a, { r: zeros(n), rBack: zeros(n), t: p, tBack: p }), b));
};
const coherent = coherentAt(s.spacerThickness);
const ai = intensityS(a, fl, fm), bi = intensityS(b, fm, fr);
const first = column(ai.t, center), loop = times(ai.rBack, bi.r);
// Exclude unreachable trapped channels (e.g. TIR modes of a uniform slab),
// whose arbitrary population otherwise makes I-Q singular.
const reachable = new Set<number>();
first.re.forEach((v, i) => { if (v > 1e-24) reachable.add(i); });
for (let pass = 0; pass < n; pass++) for (const j of [...reachable]) for (let i = 0; i < n; i++) if (loop.re[i * n + j] > 1e-24) reachable.add(i);
const indices = [...reachable], system = eye(indices.length), rhs = zeros(indices.length, 1);
indices.forEach((i, row) => { rhs.re[row] = first.re[i]; indices.forEach((j, col) => { system.re[row * indices.length + col] -= loop.re[i * n + j]; }); });
const totalDown = zeros(n, 1);
if (indices.length) { const reduced = solve(system, rhs); indices.forEach((i, row) => { totalDown.re[i] = reduced.re[row]; }); }
const reflected = plus(column(ai.r, center), times(ai.tBack, times(bi.r, totalDown)));
const transmitted = times(bi.t, totalDown);
let incoherent = powers(Array.from(reflected.re), Array.from(transmitted.re));
let passes = [{ reflection: sum(Array.from(column(ai.r, center).re)), transmission: 0, remaining: sum(Array.from(first.re)) }];
let down = first;
for (let i = 0; i < 8; i++) {
const up = times(bi.r, down), r = times(ai.tBack, up), t = times(bi.t, down);
down = times(ai.rBack, up);
passes.push({ reflection: sum(Array.from(r.re)), transmission: sum(Array.from(t.re)), remaining: sum(Array.from(down.re)) });
}
if (s.preserveSymmetry && kx.some((k, i) => kx.some((v, j) => i !== j && fm[i] > 0 && fm[j] > 0 && Math.abs(k*k-v*v) < 1e-12))) {
const symmetric = symmetricSum(a,b,fl,fm,fr,kx,center); incoherent = symmetric.power; passes = symmetric.passes;
}
let averaged = coherent;
if (s.thicknessSpread > 0) {
const r = Array(n).fill(0), t = Array(n).fill(0); let weights = 0;
for (let i = 0; i < s.averageSamples; i++) {
const z = -3 + 6 * (i + .5) / s.averageSamples, weight = Math.exp(-z * z / 2);
const p = coherentAt(s.spacerThickness + z * s.thicknessSpread); weights += weight;
for (let j = 0; j < n; j++) { r[j] += weight * p.r[j]; t[j] += weight * p.t[j]; }
}
averaged = powers(r.map(v => v / weights), t.map(v => v / weights));
}
const warnings: string[] = [];
if ([coherent, incoherent, averaged].some(p => p.absorption < -1e-6 || p.absorption > 1 + 1e-6)) warnings.push('The passive power bounds are violated. Treat this setting as numerically unreliable.');
if (!s.preserveSymmetry && Math.abs(s.angle) < 1e-8) warnings.push('At normal incidence, symmetric ±m channels can retain mutual coherence. This calculation currently uses the original thesis’s fully decorrelated-order model.');
if ([-center - 1, center + 1].some(m => Math.abs(lateral + m * s.wavelength / s.period) < Math.max(s.incidentIndex, s.spacerIndex, s.substrateIndex))) warnings.push('Some propagating orders are omitted. Increase the Fourier harmonics.');
const decay = kx.filter(k => k * k > s.spacerIndex ** 2).map(k => Math.exp(-2 * Math.PI * Math.sqrt(k * k - s.spacerIndex ** 2) * s.spacerThickness / s.wavelength));
if (decay.some(v => v > 1e-4)) warnings.push('An evanescent order can cross this layer. The thick-layer incoherent approximation may be inadequate near this cutoff.');
if (s.thicknessSpread > 0 && 4 * Math.PI * s.spacerIndex / s.wavelength * (6 * s.thicknessSpread / s.averageSamples) > .5) warnings.push('Thickness averaging may be undersampled. Increase the thickness samples and compare.');
const angle = (k: number, index: number) => Math.abs(k) < index ? Math.asin(k / index) * 180 / Math.PI : null;
return { settings: { ...s }, coherent, incoherent, averaged, passes, warnings,
orders: kx.map((k, i) => ({ order: i - center, angleR: angle(k, s.incidentIndex), angleT: angle(k, s.substrateIndex), angleInside: angle(k, s.spacerIndex) })) };
}
/** Retain equal-kz cross-order coherences (2024 paper, symmetric interactions).
* The state contains only pairs that survive a thick-layer traversal. Thin
* subsystems transform C as S C S† before projecting onto those pairs again. */
function symmetricSum(a: Scattering, b: Scattering, fl: number[], fm: number[], fr: number[], kx: number[], center: number) {
const n = fm.length, pairs: [number, number][] = [];
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) if (fm[i] > 0 && fm[j] > 0 && Math.abs(kx[i] ** 2 - kx[j] ** 2) < 1e-12) pairs.push([i, j]);
const normalized = (s: Dense, left: number[], right: number[]) => {
const out = zeros(s.rows, s.cols);
for (let i = 0; i < s.rows; i++) for (let j = 0; j < s.cols; j++) if (left[j] > 0 && right[i] > 0) {
const f = Math.sqrt(right[i] / left[j]), p = i * s.cols + j; out.re[p] = s.re[p] * f; out.im[p] = s.im[p] * f;
}
return out;
};
const rTop = normalized(a.rBack, fm, fm), rBottom = normalized(b.r, fm, fm);
const tTop = normalized(a.tBack, fm, fl), tBottom = normalized(b.t, fm, fr), initial = normalized(a.t, fl, fm);
const product = (a: ReturnType<typeof complex>, b: ReturnType<typeof complex>) => complex(a.re * b.re + a.im * b.im, a.im * b.re - a.re * b.im);
const operator = (s: Dense, outputPairs: [number, number][]) => {
const op = zeros(outputPairs.length, pairs.length);
outputPairs.forEach(([i, j], row) => pairs.forEach(([k, l], col) => {
const v = product(value(s, i, k), value(s, j, l)); op.re[row * op.cols + col] = v.re; op.im[row * op.cols + col] = v.im;
})); return op;
};
const first = zeros(pairs.length, 1);
pairs.forEach(([i,j], row) => { const v = product(value(initial, i, center), value(initial, j, center)); first.re[row] = v.re; first.im[row] = v.im; });
const bottom = operator(rBottom, pairs), loop = times(operator(rTop, pairs), bottom);
const reachable = new Set<number>(); first.re.forEach((v,i) => { if (Math.hypot(v,first.im[i]) > 1e-24) reachable.add(i); });
for (let pass=0;pass<pairs.length;pass++) for(const j of [...reachable]) for(let i=0;i<pairs.length;i++) if(norm(value(loop,i,j))>1e-48) reachable.add(i);
const indices=[...reachable], lhs=eye(indices.length), rhs=zeros(indices.length,1);
indices.forEach((i,row)=>{rhs.re[row]=first.re[i];rhs.im[row]=first.im[i];indices.forEach((j,col)=>{const p=row*indices.length+col;lhs.re[p]-=loop.re[i*loop.cols+j];lhs.im[p]-=loop.im[i*loop.cols+j];});});
const down=zeros(pairs.length,1);
if(indices.length){const solved=solve(lhs,rhs);indices.forEach((i,row)=>{down.re[i]=solved.re[row];down.im[i]=solved.im[row];});}
const exteriorPairs=Array.from({length:n},(_,i)=>[i,i] as [number,number]);
const outR=times(operator(tTop,exteriorPairs),bottom), outT=operator(tBottom,exteriorPairs);
const direct=column(powerMatrix(a.r,fl,fl),center), r=plus(direct,times(outR,down)), t=times(outT,down);
const trace=(v:Dense)=>pairs.reduce((total,[i,j],row)=>total+(i===j?v.re[row]:0),0);
const passes=[{reflection:sum(Array.from(direct.re)),transmission:0,remaining:trace(first)}];
let current=first;
for(let i=0;i<8;i++){const r=times(outR,current),t=times(outT,current);current=times(loop,current);passes.push({reflection:sum(Array.from(r.re)),transmission:sum(Array.from(t.re)),remaining:trace(current)});}
return {power:powers(Array.from(r.re),Array.from(t.re)),passes};
}