tmmcore 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/package.json +53 -0
- package/src/build.ps1 +121 -0
- package/src/index.js +35 -0
- package/src/tmm.js +602 -0
- package/src/tmmWasm.js +398 -0
- package/src/tmm_kernel.c +589 -0
- package/src/tmm_kernel.wasm +0 -0
package/src/tmm.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transfer-matrix method for multilayer thin films — JavaScript reference
|
|
3
|
+
* implementation.
|
|
4
|
+
*
|
|
5
|
+
* System model:
|
|
6
|
+
* incident medium (n0, θ0) → layer1 → … → layerN → substrate
|
|
7
|
+
*
|
|
8
|
+
* Conventions:
|
|
9
|
+
* ñ = n + ik k ≥ 0 for absorbing media
|
|
10
|
+
* time factor exp(−iωt) a wave exp(i(kz − ωt)) decays for k > 0
|
|
11
|
+
* off-diagonals of the characteristic matrix carry −i
|
|
12
|
+
* wavelengths and thicknesses in nm, angles in degrees from normal
|
|
13
|
+
*
|
|
14
|
+
* This is the complex conjugate of Macleod's convention (ñ = n − ik, exp(+iωt),
|
|
15
|
+
* +i on the off-diagonals). R, T and A are identical under conjugation; phase-
|
|
16
|
+
* sensitive quantities negate the raw TMM phase to recover Macleod's sign.
|
|
17
|
+
*
|
|
18
|
+
* Complex numbers are [re, im] pairs. All arithmetic is double precision. The C
|
|
19
|
+
* kernel shipped alongside is a line-by-line port of this file and agrees with
|
|
20
|
+
* it to float64 round-off (see tests/).
|
|
21
|
+
*
|
|
22
|
+
* References:
|
|
23
|
+
* • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125
|
|
24
|
+
* • Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996), Eqs. (3)–(6)
|
|
25
|
+
* • Tikhonravov, Trubetskov & DeBell, Appl. Opt. 35, 5493 (1996)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ── Complex number arithmetic ─────────────────────────────────────────────────
|
|
29
|
+
// All complex numbers are [re, im] arrays.
|
|
30
|
+
|
|
31
|
+
function cadd([ar, ai], [br, bi]) { return [ar + br, ai + bi]; }
|
|
32
|
+
function csub([ar, ai], [br, bi]) { return [ar - br, ai - bi]; }
|
|
33
|
+
function cmul([ar, ai], [br, bi]) { return [ar * br - ai * bi, ar * bi + ai * br]; }
|
|
34
|
+
function cdiv([ar, ai], [br, bi]) {
|
|
35
|
+
const d = br * br + bi * bi;
|
|
36
|
+
return [(ar * br + ai * bi) / d, (ai * br - ar * bi) / d];
|
|
37
|
+
}
|
|
38
|
+
function cabs2([ar, ai]) { return ar * ar + ai * ai; }
|
|
39
|
+
function cconj([ar, ai]) { return [ar, -ai]; }
|
|
40
|
+
function csqrt([ar, ai]) {
|
|
41
|
+
const r = Math.sqrt(Math.sqrt(ar * ar + ai * ai));
|
|
42
|
+
const theta = Math.atan2(ai, ar) / 2;
|
|
43
|
+
return [r * Math.cos(theta), r * Math.sin(theta)];
|
|
44
|
+
}
|
|
45
|
+
function ccos([ar, ai]) {
|
|
46
|
+
return [Math.cos(ar) * Math.cosh(ai), -Math.sin(ar) * Math.sinh(ai)];
|
|
47
|
+
}
|
|
48
|
+
function csin([ar, ai]) {
|
|
49
|
+
return [Math.sin(ar) * Math.cosh(ai), Math.cos(ar) * Math.sinh(ai)];
|
|
50
|
+
}
|
|
51
|
+
function creal([ar]) { return ar; }
|
|
52
|
+
function cimag([, ai]) { return ai; }
|
|
53
|
+
|
|
54
|
+
// ── 2×2 complex matrix multiply ───────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function matmul(A, B) {
|
|
57
|
+
return [
|
|
58
|
+
[
|
|
59
|
+
cadd(cmul(A[0][0], B[0][0]), cmul(A[0][1], B[1][0])),
|
|
60
|
+
cadd(cmul(A[0][0], B[0][1]), cmul(A[0][1], B[1][1]))
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
cadd(cmul(A[1][0], B[0][0]), cmul(A[1][1], B[1][0])),
|
|
64
|
+
cadd(cmul(A[1][0], B[0][1]), cmul(A[1][1], B[1][1]))
|
|
65
|
+
]
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const MATRIX_RESCALE_THRESHOLD = 1e100;
|
|
70
|
+
|
|
71
|
+
// A common real scale factor cancels from reflectance (Macleod 5th ed.,
|
|
72
|
+
// Eq. 2.123, p. 45). Callers retain it for transmittance (Eq. 2.125) while the
|
|
73
|
+
// bounded characteristic-matrix product prevents opaque-stack overflow.
|
|
74
|
+
function rescaleMatrix(M) {
|
|
75
|
+
let scale = 0;
|
|
76
|
+
for (const row of M) {
|
|
77
|
+
for (const [re, im] of row) {
|
|
78
|
+
scale = Math.max(scale, Math.abs(re), Math.abs(im));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (scale <= MATRIX_RESCALE_THRESHOLD) return 0;
|
|
82
|
+
const inverse = 1 / scale;
|
|
83
|
+
for (const row of M) {
|
|
84
|
+
for (const value of row) {
|
|
85
|
+
value[0] *= inverse;
|
|
86
|
+
value[1] *= inverse;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return Math.log(scale);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Snell's law ───────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function snellCosTheta(n0, sinTheta0, nj) {
|
|
95
|
+
// sinThetaJ = n0 * sinTheta0 / nj (complex)
|
|
96
|
+
const sinThetaJ = cdiv(cmul(n0, sinTheta0), nj);
|
|
97
|
+
// cosTheta = sqrt(1 - sin²θ)
|
|
98
|
+
return csqrt(csub([1, 0], cmul(sinThetaJ, sinThetaJ)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Layer characteristic matrix ───────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function layerMatrix(nj, dj_nm, lambda_nm, cosTheta_j, pol) {
|
|
104
|
+
// Phase thickness: delta = (2π/λ) * n * d * cosθ (complex)
|
|
105
|
+
const k0 = (2 * Math.PI) / lambda_nm;
|
|
106
|
+
const delta = cmul(cmul(nj, [k0 * dj_nm, 0]), cosTheta_j);
|
|
107
|
+
|
|
108
|
+
// Numerical-overflow guard for very thick ABSORBING layers (k>0). ccos/csin
|
|
109
|
+
// use cosh/sinh(Im δ), and cosh(710)=Inf → the whole TMM returns NaN. But by
|
|
110
|
+
// |Im δ| ≳ a few tens the layer is already optically opaque (single-pass
|
|
111
|
+
// transmittance e^{−2 Im δ} ≈ 0) AND the surface reflectance has fully
|
|
112
|
+
// converged, so clamping Im δ here is exact to machine precision while
|
|
113
|
+
// keeping the characteristic matrix finite. For non-absorbing / thin layers
|
|
114
|
+
// (|Im δ| < MAX_IM_DELTA) this is a no-op → results are bit-identical.
|
|
115
|
+
const MAX_IM_DELTA = 50; // e^{−100} ≈ 4e−44 ; cosh(50) ≈ 2.6e21 (safe under products)
|
|
116
|
+
if (delta[1] > MAX_IM_DELTA) delta[1] = MAX_IM_DELTA;
|
|
117
|
+
else if (delta[1] < -MAX_IM_DELTA) delta[1] = -MAX_IM_DELTA;
|
|
118
|
+
|
|
119
|
+
const cosD = ccos(delta);
|
|
120
|
+
const sinD = csin(delta);
|
|
121
|
+
|
|
122
|
+
// Admittance eta
|
|
123
|
+
let eta;
|
|
124
|
+
if (pol === 's') {
|
|
125
|
+
eta = cmul(nj, cosTheta_j); // n cosθ
|
|
126
|
+
} else {
|
|
127
|
+
eta = cdiv(nj, cosTheta_j); // n / cosθ
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// M = [[cosD, -i sinD / eta], [-i eta sinD, cosD]]
|
|
131
|
+
const iSinD_div_eta = cmul([0, -1], cdiv(sinD, eta));
|
|
132
|
+
const iEta_sinD = cmul([0, -1], cmul(eta, sinD));
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
[cosD, iSinD_div_eta],
|
|
136
|
+
[iEta_sinD, cosD ]
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Core TMM for one wavelength ───────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {number} lambda_nm wavelength in nm
|
|
144
|
+
* @param {number} theta_deg angle of incidence in degrees
|
|
145
|
+
* @param {string} pol 's' or 'p'
|
|
146
|
+
* @param {[number,number]} n0 complex n of incident medium
|
|
147
|
+
* @param {[number,number]} ns complex n of substrate (exit medium)
|
|
148
|
+
* @param {{ n:[number,number], d:number }[]} layers each layer { n: [re,im], d: thickness_nm }
|
|
149
|
+
* @returns {{ R:number, T:number, A:number }}
|
|
150
|
+
*/
|
|
151
|
+
export function tmm(lambda_nm, theta_deg, pol, n0, ns, layers) {
|
|
152
|
+
const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
|
|
153
|
+
const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
|
|
154
|
+
|
|
155
|
+
// Admittance of incident medium
|
|
156
|
+
const eta0 = pol === 's'
|
|
157
|
+
? cmul(n0, cosTheta0)
|
|
158
|
+
: cdiv(n0, cosTheta0);
|
|
159
|
+
|
|
160
|
+
// Admittance of substrate
|
|
161
|
+
const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
|
|
162
|
+
const etaS = pol === 's'
|
|
163
|
+
? cmul(ns, cosThetaS)
|
|
164
|
+
: cdiv(ns, cosThetaS);
|
|
165
|
+
|
|
166
|
+
// Build total transfer matrix M = M1 × M2 × ... × MN
|
|
167
|
+
let M = [[ [1, 0], [0, 0] ], [ [0, 0], [1, 0] ]]; // identity
|
|
168
|
+
let logScale = 0;
|
|
169
|
+
|
|
170
|
+
for (const { n, d } of layers) {
|
|
171
|
+
if (d <= 0) continue;
|
|
172
|
+
const cosThetaJ = snellCosTheta(n0, sinTheta0, n);
|
|
173
|
+
const Mj = layerMatrix(n, d, lambda_nm, cosThetaJ, pol);
|
|
174
|
+
M = matmul(M, Mj);
|
|
175
|
+
logScale += rescaleMatrix(M);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// [B, C]^T = M × [1, eta_s]^T
|
|
179
|
+
const B = cadd(M[0][0], cmul(M[0][1], etaS));
|
|
180
|
+
const C = cadd(M[1][0], cmul(M[1][1], etaS));
|
|
181
|
+
|
|
182
|
+
// r = (η0 B - C) / (η0 B + C)
|
|
183
|
+
const eta0B = cmul(eta0, B);
|
|
184
|
+
const r = cdiv(csub(eta0B, C), cadd(eta0B, C));
|
|
185
|
+
|
|
186
|
+
// t = 2 η0 / (η0 B + C)
|
|
187
|
+
const t = cdiv(cmul([2, 0], eta0), cadd(eta0B, C));
|
|
188
|
+
|
|
189
|
+
const R = cabs2(r);
|
|
190
|
+
|
|
191
|
+
// T = Re(etaS) / Re(eta0) * |t|²
|
|
192
|
+
const T = Math.max(0, creal(etaS) / creal(eta0) * cabs2(t) * Math.exp(-2 * logScale));
|
|
193
|
+
|
|
194
|
+
const A = Math.max(0, 1 - R - T);
|
|
195
|
+
|
|
196
|
+
return { R, T, A };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Analytic needle P-function kernel ─────────────────────────────────────────
|
|
200
|
+
//
|
|
201
|
+
// Returns the ANALYTIC merit-function gradient dF/dd of inserting an
|
|
202
|
+
// infinitesimally thin needle, for every insertion position × candidate
|
|
203
|
+
// material, at one (λ, θ, pol). This is the d→0 limit of Sullivan's
|
|
204
|
+
// numerical pre/post method, i.e. Tikhonravov's analytic P-function.
|
|
205
|
+
//
|
|
206
|
+
// Derivation (citations):
|
|
207
|
+
// • Characteristic matrix & [B,C], r, t: Macleod, Thin-Film Optical
|
|
208
|
+
// Filters 5th ed., §2.4 Eqs. 2.111, 2.123–2.125 (JS sign convention:
|
|
209
|
+
// off-diagonals carry −i, see layerMatrix above).
|
|
210
|
+
// • Pre/post decomposition M = M_pre · M_k · M_post and needle
|
|
211
|
+
// insertion: Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996),
|
|
212
|
+
// Eqs. (3)–(6).
|
|
213
|
+
// • Needle series dF = P₁ d + P₂ d² + …, insert where P₁<0:
|
|
214
|
+
// Tikhonravov, Trubetskov & DeBell, Appl. Opt. 35, 5493 (1996),
|
|
215
|
+
// Eqs. (1)–(2).
|
|
216
|
+
//
|
|
217
|
+
// Needle matrix of index nₐ, thickness d:
|
|
218
|
+
// M_n(δ) = [[cosδ, −i sinδ/ηₐ], [−i ηₐ sinδ, cosδ]], δ = (2π/λ) nₐ d cosθₐ
|
|
219
|
+
// As d→0: M_n = I + A·d + O(d²), with
|
|
220
|
+
// A = [[0, −i Q/ηₐ], [−i ηₐ Q, 0]], Q = (2π/λ) nₐ cosθₐ.
|
|
221
|
+
// Insertion at gap `pos`: [B,C] = Pre·Post, and to first order
|
|
222
|
+
// d[B,C]/dd = Pre · A · Post.
|
|
223
|
+
// Then with den = η₀B + C:
|
|
224
|
+
// dr/dd = (2η₀/den²)·(C·dB − B·dC), dR/dd = 2 Re[ r̄ · dr/dd ]
|
|
225
|
+
// dt/dd = −(2η₀/den²)·(η₀·dB + dC), dT/dd = (Re ηs/Re η₀)·2 Re[ t̄ · dt/dd ]
|
|
226
|
+
// dA/dd = −(dR/dd + dT/dd)
|
|
227
|
+
// The host layer cancels automatically through Pre/Post (a needle of the
|
|
228
|
+
// host index at an interior point gives ~0), so no nₐ²−n_host² term is
|
|
229
|
+
// needed — exactly as in Sullivan's scheme.
|
|
230
|
+
//
|
|
231
|
+
// Returns { R, T, A, gaps, intra } where
|
|
232
|
+
// gaps[pos] = [{dR,dT,dA} per candidate] pos = 0..N
|
|
233
|
+
// intra[k][fi] = { frac, perCand:[{dR,dT,dA}] } (host-split)
|
|
234
|
+
function cmatvec(M, v) {
|
|
235
|
+
return [
|
|
236
|
+
cadd(cmul(M[0][0], v[0]), cmul(M[0][1], v[1])),
|
|
237
|
+
cadd(cmul(M[1][0], v[0]), cmul(M[1][1], v[1])),
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function tmmNeedleScan(lambda_nm, theta_deg, pol, n0, ns, layers,
|
|
242
|
+
candidateNs, intraFracs = []) {
|
|
243
|
+
const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
|
|
244
|
+
const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
|
|
245
|
+
const eta0 = pol === 's' ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
|
|
246
|
+
const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
|
|
247
|
+
const etaS = pol === 's' ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
|
|
248
|
+
|
|
249
|
+
// NOTE: layers are used as-is (no d>0 filter) so gap/intra indices match
|
|
250
|
+
// the caller's design.frontLayers exactly. A zero-thickness layer yields
|
|
251
|
+
// an identity characteristic matrix (δ=0), which is harmless.
|
|
252
|
+
const valid = layers;
|
|
253
|
+
const N = valid.length;
|
|
254
|
+
const cosThJ = valid.map(({ n }) => snellCosTheta(n0, sinTheta0, n));
|
|
255
|
+
const Ms = valid.map(({ n, d }, k) => layerMatrix(n, Math.max(d, 0), lambda_nm, cosThJ[k], pol));
|
|
256
|
+
|
|
257
|
+
const I = [[[1, 0], [0, 0]], [[0, 0], [1, 0]]];
|
|
258
|
+
// Pre[j] = M_0·…·M_{j-1}; Post[j] = M_j·…·M_{N-1}·[1,ηs]
|
|
259
|
+
const Pre = new Array(N + 1);
|
|
260
|
+
Pre[0] = I;
|
|
261
|
+
for (let j = 0; j < N; j++) Pre[j + 1] = matmul(Pre[j], Ms[j]);
|
|
262
|
+
const Post = new Array(N + 1);
|
|
263
|
+
Post[N] = [[1, 0], etaS];
|
|
264
|
+
for (let j = N - 1; j >= 0; j--) Post[j] = cmatvec(Ms[j], Post[j + 1]);
|
|
265
|
+
|
|
266
|
+
// Base spectral quantities from the full matrix
|
|
267
|
+
const Bv = Post[0][0], Cv = Post[0][1];
|
|
268
|
+
const den = cadd(cmul(eta0, Bv), Cv);
|
|
269
|
+
const den2 = cmul(den, den);
|
|
270
|
+
const r = cdiv(csub(cmul(eta0, Bv), Cv), den);
|
|
271
|
+
const t = cdiv(cmul([2, 0], eta0), den);
|
|
272
|
+
const R = cabs2(r);
|
|
273
|
+
const Tfac = creal(etaS) / creal(eta0);
|
|
274
|
+
const T = Math.max(0, Tfac * cabs2(t));
|
|
275
|
+
const A = Math.max(0, 1 - R - T);
|
|
276
|
+
|
|
277
|
+
const k0 = (2 * Math.PI) / lambda_nm;
|
|
278
|
+
|
|
279
|
+
// Needle derivative-matrix A for a candidate index, at a given cosθ.
|
|
280
|
+
function needleA(nA) {
|
|
281
|
+
const cthA = snellCosTheta(n0, sinTheta0, nA);
|
|
282
|
+
const etaA = pol === 's' ? cmul(nA, cthA) : cdiv(nA, cthA);
|
|
283
|
+
const Q = cmul(cmul(nA, [k0, 0]), cthA); // (2π/λ) nₐ cosθₐ
|
|
284
|
+
return [
|
|
285
|
+
[[0, 0], cmul([0, -1], cdiv(Q, etaA))], // −i Q/ηₐ
|
|
286
|
+
[cmul([0, -1], cmul(etaA, Q)), [0, 0]], // −i ηₐ Q
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Given d[B,C]/dd = (dB,dC), produce {dR,dT,dA}.
|
|
291
|
+
function metrics(dB, dC) {
|
|
292
|
+
// dr/dd = (2η₀/den²)(C·dB − B·dC)
|
|
293
|
+
const f = cdiv(cmul([2, 0], eta0), den2);
|
|
294
|
+
const dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
|
|
295
|
+
const dR = 2 * creal(cmul(cconj(r), dr));
|
|
296
|
+
// dt/dd = −(2η₀/den²)(η₀·dB + dC)
|
|
297
|
+
const dt = cmul([-1, 0], cmul(f, cadd(cmul(eta0, dB), dC)));
|
|
298
|
+
const dT = Tfac * 2 * creal(cmul(cconj(t), dt));
|
|
299
|
+
return { dR, dT, dA: -(dR + dT) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Precompute A·Post[pos] is position-dependent; do per (pos, cand).
|
|
303
|
+
const Acache = candidateNs.map(needleA);
|
|
304
|
+
|
|
305
|
+
const gaps = new Array(N + 1);
|
|
306
|
+
for (let pos = 0; pos <= N; pos++) {
|
|
307
|
+
const pre = Pre[pos], post = Post[pos];
|
|
308
|
+
gaps[pos] = Acache.map(Amat => {
|
|
309
|
+
const dV = cmatvec(pre, cmatvec(Amat, post));
|
|
310
|
+
return metrics(dV[0], dV[1]);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const intra = [];
|
|
315
|
+
if (intraFracs.length) {
|
|
316
|
+
for (let k = 0; k < N; k++) {
|
|
317
|
+
const { n, d } = valid[k];
|
|
318
|
+
const cth = cosThJ[k];
|
|
319
|
+
const rowK = [];
|
|
320
|
+
for (const frac of intraFracs) {
|
|
321
|
+
const Mleft = layerMatrix(n, Math.max(frac * d, 1e-9), lambda_nm, cth, pol);
|
|
322
|
+
const Mright = layerMatrix(n, Math.max((1 - frac) * d, 1e-9), lambda_nm, cth, pol);
|
|
323
|
+
const preIn = matmul(Pre[k], Mleft);
|
|
324
|
+
const postIn = cmatvec(Mright, Post[k + 1]);
|
|
325
|
+
rowK.push({
|
|
326
|
+
frac,
|
|
327
|
+
perCand: Acache.map(Amat => {
|
|
328
|
+
const dV = cmatvec(preIn, cmatvec(Amat, postIn));
|
|
329
|
+
return metrics(dV[0], dV[1]);
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
intra.push(rowK);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { R, T, A, gaps, intra, N };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Analytic thickness-Jacobian kernel ────────────────────────────────────────
|
|
341
|
+
//
|
|
342
|
+
// Returns the EXACT analytic derivatives dR/dd_k, dT/dd_k, dA/dd_k of every
|
|
343
|
+
// existing layer's thickness, at one (λ, θ, pol). Replaces the central-
|
|
344
|
+
// difference Jacobian in the DLS refiner (2·N fewer TMM evals per step).
|
|
345
|
+
//
|
|
346
|
+
// Derivation (citations):
|
|
347
|
+
// • Characteristic matrix Eq. 2.111 and product form Eq. 2.113 with
|
|
348
|
+
// δ_r = 2π N_r d_r cosθ_r / λ
|
|
349
|
+
// Macleod, Thin-Film Optical Filters 5th ed., §2.4 (verified verbatim).
|
|
350
|
+
// This module's documented sign convention puts −i on the off-diagonals
|
|
351
|
+
// (see file header & layerMatrix); the derivative below is taken of THAT
|
|
352
|
+
// matrix, not Macleod's +i form, so it stays byte-consistent with tmm().
|
|
353
|
+
// • Pre/post decomposition [B,C] = Pre·M_k·Post and the parametric
|
|
354
|
+
// derivative ∂[B,C]/∂p = Pre·(∂M_k/∂p)·Post:
|
|
355
|
+
// Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996), Eqs. (3)–(6).
|
|
356
|
+
//
|
|
357
|
+
// Only δ depends on d_k (η = n cosθ for s, n/cosθ for p, and cosθ depend on
|
|
358
|
+
// n, θ, λ only). With Q ≡ dδ/dd = (2π/λ) n cosθ:
|
|
359
|
+
//
|
|
360
|
+
// dM_k/dd_k = Q · [[ −sinδ, −i cosδ / η ],
|
|
361
|
+
// [ −i η cosδ, −sinδ ]]
|
|
362
|
+
//
|
|
363
|
+
// As δ→0 this collapses to [[0,−iQ/η],[−iQη,0]] — exactly the needle
|
|
364
|
+
// A-matrix in tmmNeedleScan (needleA), i.e. the needle kernel is the δ=0
|
|
365
|
+
// special case of this; a strong internal-consistency check.
|
|
366
|
+
//
|
|
367
|
+
// The {dR,dT,dA} chain rule below is identical (verbatim) to the validated
|
|
368
|
+
// `metrics()` in tmmNeedleScan.
|
|
369
|
+
export function tmmThicknessJacobian(lambda_nm, theta_deg, pol, n0, ns, layers) {
|
|
370
|
+
const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
|
|
371
|
+
const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
|
|
372
|
+
const eta0 = pol === 's' ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
|
|
373
|
+
const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
|
|
374
|
+
const etaS = pol === 's' ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
|
|
375
|
+
|
|
376
|
+
const valid = layers; // used as-is (index parity, see needle)
|
|
377
|
+
const N = valid.length;
|
|
378
|
+
const cosThJ = valid.map(({ n }) => snellCosTheta(n0, sinTheta0, n));
|
|
379
|
+
const Ms = valid.map(({ n, d }, k) =>
|
|
380
|
+
layerMatrix(n, Math.max(d, 0), lambda_nm, cosThJ[k], pol));
|
|
381
|
+
|
|
382
|
+
const I = [[[1, 0], [0, 0]], [[0, 0], [1, 0]]];
|
|
383
|
+
// Pre[j] = M_0·…·M_{j-1}; Post[j] = M_j·…·M_{N-1}·[1,ηs]
|
|
384
|
+
const Pre = new Array(N + 1);
|
|
385
|
+
Pre[0] = I;
|
|
386
|
+
for (let j = 0; j < N; j++) Pre[j + 1] = matmul(Pre[j], Ms[j]);
|
|
387
|
+
const Post = new Array(N + 1);
|
|
388
|
+
Post[N] = [[1, 0], etaS];
|
|
389
|
+
for (let j = N - 1; j >= 0; j--) Post[j] = cmatvec(Ms[j], Post[j + 1]);
|
|
390
|
+
|
|
391
|
+
// Base spectral quantities from the full matrix (same as tmmNeedleScan).
|
|
392
|
+
const Bv = Post[0][0], Cv = Post[0][1];
|
|
393
|
+
const den = cadd(cmul(eta0, Bv), Cv);
|
|
394
|
+
const den2 = cmul(den, den);
|
|
395
|
+
const r = cdiv(csub(cmul(eta0, Bv), Cv), den);
|
|
396
|
+
const t = cdiv(cmul([2, 0], eta0), den);
|
|
397
|
+
const R = cabs2(r);
|
|
398
|
+
const Tfac = creal(etaS) / creal(eta0);
|
|
399
|
+
const T = Math.max(0, Tfac * cabs2(t));
|
|
400
|
+
const A = Math.max(0, 1 - R - T);
|
|
401
|
+
|
|
402
|
+
// [B,C] → {dR,dT,dA} (verbatim from validated tmmNeedleScan.metrics)
|
|
403
|
+
const f = cdiv(cmul([2, 0], eta0), den2);
|
|
404
|
+
function metrics(dB, dC) {
|
|
405
|
+
const dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
|
|
406
|
+
const dR = 2 * creal(cmul(cconj(r), dr));
|
|
407
|
+
const dt = cmul([-1, 0], cmul(f, cadd(cmul(eta0, dB), dC)));
|
|
408
|
+
const dT = Tfac * 2 * creal(cmul(cconj(t), dt));
|
|
409
|
+
return { dR, dT, dA: -(dR + dT) };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const k0 = (2 * Math.PI) / lambda_nm;
|
|
413
|
+
const dRdd = new Array(N), dTdd = new Array(N), dAdd = new Array(N);
|
|
414
|
+
for (let k = 0; k < N; k++) {
|
|
415
|
+
const { n, d } = valid[k];
|
|
416
|
+
const cth = cosThJ[k];
|
|
417
|
+
const etaK = pol === 's' ? cmul(n, cth) : cdiv(n, cth);
|
|
418
|
+
const Q = cmul(cmul(n, [k0, 0]), cth); // (2π/λ) n cosθ
|
|
419
|
+
const delta = cmul(cmul(n, [k0 * Math.max(d, 0), 0]), cth);
|
|
420
|
+
const cD = ccos(delta), sD = csin(delta);
|
|
421
|
+
// dM_k/dd_k = Q · [[ −sinδ, −i cosδ/η ], [ −i η cosδ, −sinδ ]]
|
|
422
|
+
const dMk = [
|
|
423
|
+
[ cmul(Q, cmul([-1, 0], sD)), cmul(Q, cmul([0, -1], cdiv(cD, etaK))) ],
|
|
424
|
+
[ cmul(Q, cmul([0, -1], cmul(etaK, cD))), cmul(Q, cmul([-1, 0], sD)) ],
|
|
425
|
+
];
|
|
426
|
+
const dV = cmatvec(Pre[k], cmatvec(dMk, Post[k + 1]));
|
|
427
|
+
const m = metrics(dV[0], dV[1]);
|
|
428
|
+
dRdd[k] = m.dR; dTdd[k] = m.dT; dAdd[k] = m.dA;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { R, T, A, dRdd, dTdd, dAdd, N };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Analytic thickness-Hessian kernel ─────────────────────────────────────────
|
|
435
|
+
//
|
|
436
|
+
// Returns the EXACT analytic SECOND derivatives ∂²R/∂dᵢ∂dⱼ, ∂²T/∂dᵢ∂dⱼ,
|
|
437
|
+
// ∂²A/∂dᵢ∂dⱼ (full N×N symmetric matrices) plus the first derivatives, at one
|
|
438
|
+
// (λ, θ, pol). This is the second-order extension of tmmThicknessJacobian and
|
|
439
|
+
// enables true Newton refinement (Tikhonov–Tikhonravov–Trubetskov, "Second
|
|
440
|
+
// order optimization methods in the synthesis of multilayer coatings," Comp.
|
|
441
|
+
// Maths. Math. Phys. 33, 1339 (1993)).
|
|
442
|
+
//
|
|
443
|
+
// Derivation (same Abelès matrix calculus as the Jacobian — Macleod Eq.
|
|
444
|
+
// 2.111/2.113; pre/post decomposition Sullivan & Dobrowolski 1996):
|
|
445
|
+
// [B,C] = M₀···M_{N-1}·[1,ηs]; ∂[B,C]/∂dₖ = Pre[k]·(dMₖ)·Post[k+1].
|
|
446
|
+
// Mixed second partials (i < j, position-ordered):
|
|
447
|
+
// ∂²[B,C]/∂dᵢ∂dⱼ = Pre[i]·dMᵢ·(M_{i+1}···M_{j-1})·dMⱼ·Post[j+1]
|
|
448
|
+
// Diagonal (i = j):
|
|
449
|
+
// ∂²[B,C]/∂dᵢ² = Pre[i]·(d²Mᵢ/ddᵢ²)·Post[i+1],
|
|
450
|
+
// d²Mₖ/ddₖ² = Q²·[[ −cosδ, i sinδ/η ], [ i η sinδ, −cosδ ]], Q ≡ (2π/λ)n cosθ
|
|
451
|
+
// (d²Mₖ is the d-derivative of dMₖ = Q[[−sinδ,−i cosδ/η],[−iη cosδ,−sinδ]];
|
|
452
|
+
// as δ→0 it → Q²·[[−1,0],[0,−1]], the curvature of an emerging needle.)
|
|
453
|
+
//
|
|
454
|
+
// Second-order chain rule R = |r|², r = (η₀B−C)/den, den = η₀B+C, f = 2η₀/den²:
|
|
455
|
+
// drₖ = f(C dBₖ − B dCₖ), ddenₖ = η₀ dBₖ + dCₖ
|
|
456
|
+
// d²r_ij = f(dCᵢdBⱼ + C d²B_ij − dBᵢdCⱼ − B d²C_ij) − 2·drⱼ·ddenᵢ/den
|
|
457
|
+
// d²R_ij = 2 Re( conj(drᵢ)drⱼ + conj(r) d²r_ij )
|
|
458
|
+
// t = 2η₀/den, dtₖ = −f·ddenₖ
|
|
459
|
+
// d²t_ij = −2η₀ d²den_ij/den² + 4η₀ ddenᵢddenⱼ/den³, d²den_ij = η₀ d²B_ij + d²C_ij
|
|
460
|
+
// d²T_ij = Tfac·2 Re( conj(dtᵢ)dtⱼ + conj(t) d²t_ij ), d²A = −(d²R + d²T)
|
|
461
|
+
//
|
|
462
|
+
// Cost: O(N²) small-matrix ops per (λ,θ,pol) via cached Pre/Post + an
|
|
463
|
+
// incrementally-built middle product. *Must be FD-validated before trust
|
|
464
|
+
// (tests/hessian_fd_validation.mjs).*
|
|
465
|
+
export function tmmThicknessHessian(lambda_nm, theta_deg, pol, n0, ns, layers) {
|
|
466
|
+
const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
|
|
467
|
+
const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
|
|
468
|
+
const eta0 = pol === 's' ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
|
|
469
|
+
const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
|
|
470
|
+
const etaS = pol === 's' ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
|
|
471
|
+
|
|
472
|
+
const valid = layers;
|
|
473
|
+
const N = valid.length;
|
|
474
|
+
const cosThJ = valid.map(({ n }) => snellCosTheta(n0, sinTheta0, n));
|
|
475
|
+
const Ms = valid.map(({ n, d }, k) =>
|
|
476
|
+
layerMatrix(n, Math.max(d, 0), lambda_nm, cosThJ[k], pol));
|
|
477
|
+
|
|
478
|
+
const I = [[[1, 0], [0, 0]], [[0, 0], [1, 0]]];
|
|
479
|
+
const Pre = new Array(N + 1);
|
|
480
|
+
Pre[0] = I;
|
|
481
|
+
for (let j = 0; j < N; j++) Pre[j + 1] = matmul(Pre[j], Ms[j]);
|
|
482
|
+
const Post = new Array(N + 1);
|
|
483
|
+
Post[N] = [[1, 0], etaS];
|
|
484
|
+
for (let j = N - 1; j >= 0; j--) Post[j] = cmatvec(Ms[j], Post[j + 1]);
|
|
485
|
+
|
|
486
|
+
const Bv = Post[0][0], Cv = Post[0][1];
|
|
487
|
+
const den = cadd(cmul(eta0, Bv), Cv);
|
|
488
|
+
const den2 = cmul(den, den);
|
|
489
|
+
const den3 = cmul(den2, den);
|
|
490
|
+
const r = cdiv(csub(cmul(eta0, Bv), Cv), den);
|
|
491
|
+
const t = cdiv(cmul([2, 0], eta0), den);
|
|
492
|
+
const R = cabs2(r);
|
|
493
|
+
const Tfac = creal(etaS) / creal(eta0);
|
|
494
|
+
const T = Math.max(0, Tfac * cabs2(t));
|
|
495
|
+
const A = Math.max(0, 1 - R - T);
|
|
496
|
+
const f = cdiv(cmul([2, 0], eta0), den2);
|
|
497
|
+
|
|
498
|
+
// First-derivative metrics (verbatim from tmmThicknessJacobian).
|
|
499
|
+
function metrics(dB, dC) {
|
|
500
|
+
const fmet = cdiv(cmul([2, 0], eta0), den2);
|
|
501
|
+
const dr = cmul(fmet, csub(cmul(Cv, dB), cmul(Bv, dC)));
|
|
502
|
+
const dR = 2 * creal(cmul(cconj(r), dr));
|
|
503
|
+
const dt = cmul([-1, 0], cmul(fmet, cadd(cmul(eta0, dB), dC)));
|
|
504
|
+
const dT = Tfac * 2 * creal(cmul(cconj(t), dt));
|
|
505
|
+
return { dR, dT, dA: -(dR + dT) };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const k0 = (2 * Math.PI) / lambda_nm;
|
|
509
|
+
// Per-layer first-derivative pieces: dM[k], its right-applied vector v[k] =
|
|
510
|
+
// dMₖ·Post[k+1], the [dB,dC] vector, and the diagonal second-derivative
|
|
511
|
+
// matrix d2M[k].
|
|
512
|
+
const dM = new Array(N), d2M = new Array(N), v = new Array(N);
|
|
513
|
+
const dB = new Array(N), dC = new Array(N);
|
|
514
|
+
const dRdd = new Array(N), dTdd = new Array(N), dAdd = new Array(N);
|
|
515
|
+
for (let k = 0; k < N; k++) {
|
|
516
|
+
const { n, d } = valid[k];
|
|
517
|
+
const cth = cosThJ[k];
|
|
518
|
+
const etaK = pol === 's' ? cmul(n, cth) : cdiv(n, cth);
|
|
519
|
+
const Q = cmul(cmul(n, [k0, 0]), cth); // (2π/λ) n cosθ
|
|
520
|
+
const Q2 = cmul(Q, Q);
|
|
521
|
+
const delta = cmul(cmul(n, [k0 * Math.max(d, 0), 0]), cth);
|
|
522
|
+
const cD = ccos(delta), sD = csin(delta);
|
|
523
|
+
// dMₖ/ddₖ = Q·[[ −sinδ, −i cosδ/η ], [ −i η cosδ, −sinδ ]]
|
|
524
|
+
dM[k] = [
|
|
525
|
+
[ cmul(Q, cmul([-1, 0], sD)), cmul(Q, cmul([0, -1], cdiv(cD, etaK))) ],
|
|
526
|
+
[ cmul(Q, cmul([0, -1], cmul(etaK, cD))), cmul(Q, cmul([-1, 0], sD)) ],
|
|
527
|
+
];
|
|
528
|
+
// d²Mₖ/ddₖ² = Q²·[[ −cosδ, i sinδ/η ], [ i η sinδ, −cosδ ]]
|
|
529
|
+
d2M[k] = [
|
|
530
|
+
[ cmul(Q2, cmul([-1, 0], cD)), cmul(Q2, cmul([0, 1], cdiv(sD, etaK))) ],
|
|
531
|
+
[ cmul(Q2, cmul([0, 1], cmul(etaK, sD))), cmul(Q2, cmul([-1, 0], cD)) ],
|
|
532
|
+
];
|
|
533
|
+
v[k] = cmatvec(dM[k], Post[k + 1]);
|
|
534
|
+
const dVk = cmatvec(Pre[k], v[k]);
|
|
535
|
+
dB[k] = dVk[0]; dC[k] = dVk[1];
|
|
536
|
+
const m = metrics(dB[k], dC[k]);
|
|
537
|
+
dRdd[k] = m.dR; dTdd[k] = m.dT; dAdd[k] = m.dA;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Second-order metrics for a pair (i,j) given the mixed partial [d2B,d2C].
|
|
541
|
+
function hessMetrics(i, j, d2Bv, d2Cv) {
|
|
542
|
+
const dBi = dB[i], dCi = dC[i], dBj = dB[j], dCj = dC[j];
|
|
543
|
+
const dr_i = cmul(f, csub(cmul(Cv, dBi), cmul(Bv, dCi)));
|
|
544
|
+
const dr_j = cmul(f, csub(cmul(Cv, dBj), cmul(Bv, dCj)));
|
|
545
|
+
const dden_i = cadd(cmul(eta0, dBi), dCi);
|
|
546
|
+
const dden_j = cadd(cmul(eta0, dBj), dCj);
|
|
547
|
+
// d²r_ij = f(dCᵢdBⱼ + C d²B − dBᵢdCⱼ − B d²C) − 2 drⱼ ddenᵢ/den
|
|
548
|
+
const innerR = csub(
|
|
549
|
+
cadd(cmul(dCi, dBj), cmul(Cv, d2Bv)),
|
|
550
|
+
cadd(cmul(dBi, dCj), cmul(Bv, d2Cv))
|
|
551
|
+
);
|
|
552
|
+
const d2r = csub(cmul(f, innerR),
|
|
553
|
+
cdiv(cmul(cmul([2, 0], dr_j), dden_i), den));
|
|
554
|
+
const d2Rij = 2 * (creal(cmul(cconj(dr_i), dr_j)) + creal(cmul(cconj(r), d2r)));
|
|
555
|
+
// T
|
|
556
|
+
const dt_i = cmul([-1, 0], cmul(f, dden_i));
|
|
557
|
+
const dt_j = cmul([-1, 0], cmul(f, dden_j));
|
|
558
|
+
const d2den = cadd(cmul(eta0, d2Bv), d2Cv);
|
|
559
|
+
const d2t = cadd(
|
|
560
|
+
cmul(cmul([-2, 0], eta0), cdiv(d2den, den2)),
|
|
561
|
+
cmul(cmul([4, 0], eta0), cdiv(cmul(dden_i, dden_j), den3))
|
|
562
|
+
);
|
|
563
|
+
const d2Tij = Tfac * 2 * (creal(cmul(cconj(dt_i), dt_j)) + creal(cmul(cconj(t), d2t)));
|
|
564
|
+
return { d2Rij, d2Tij };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const d2Rdd = Array.from({ length: N }, () => new Array(N).fill(0));
|
|
568
|
+
const d2Tdd = Array.from({ length: N }, () => new Array(N).fill(0));
|
|
569
|
+
const d2Add = Array.from({ length: N }, () => new Array(N).fill(0));
|
|
570
|
+
for (let i = 0; i < N; i++) {
|
|
571
|
+
const Wmat_i = matmul(Pre[i], dM[i]); // Pre[i]·dMᵢ (used for j>i)
|
|
572
|
+
let Cmid = I; // M_{i+1}···M_{j-1}, starts empty at j=i+1
|
|
573
|
+
for (let j = i; j < N; j++) {
|
|
574
|
+
let d2Bv, d2Cv;
|
|
575
|
+
if (j === i) {
|
|
576
|
+
const w = cmatvec(Pre[i], cmatvec(d2M[i], Post[i + 1]));
|
|
577
|
+
d2Bv = w[0]; d2Cv = w[1];
|
|
578
|
+
} else {
|
|
579
|
+
// ∂²[B,C]/∂dᵢ∂dⱼ = (Pre[i]·dMᵢ)·(M_{i+1}···M_{j-1})·(dMⱼ·Post[j+1])
|
|
580
|
+
const w = cmatvec(Wmat_i, cmatvec(Cmid, v[j]));
|
|
581
|
+
d2Bv = w[0]; d2Cv = w[1];
|
|
582
|
+
}
|
|
583
|
+
const { d2Rij, d2Tij } = hessMetrics(i, j, d2Bv, d2Cv);
|
|
584
|
+
d2Rdd[i][j] = d2Rdd[j][i] = d2Rij;
|
|
585
|
+
d2Tdd[i][j] = d2Tdd[j][i] = d2Tij;
|
|
586
|
+
d2Add[i][j] = d2Add[j][i] = -(d2Rij + d2Tij);
|
|
587
|
+
if (j >= i + 1) Cmid = matmul(Cmid, Ms[j]); // advance middle: include M_j
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { R, T, A, dRdd, dTdd, dAdd, d2Rdd, d2Tdd, d2Add, N };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Low-level primitives ─────────────────────────────────────────────────────
|
|
595
|
+
// Exported for callers building transfer-matrix variants on the same
|
|
596
|
+
// conventions. Lower level than the four functions above, and correspondingly
|
|
597
|
+
// less stable across versions.
|
|
598
|
+
|
|
599
|
+
export {
|
|
600
|
+
cadd, csub, cmul, cdiv, cabs2, cconj, csqrt, ccos, csin, creal, cimag,
|
|
601
|
+
matmul, rescaleMatrix, snellCosTheta, layerMatrix, cmatvec
|
|
602
|
+
};
|