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.
@@ -0,0 +1,589 @@
1
+ /*
2
+ * tmm_kernel.c — transfer-matrix method for multilayer thin films.
3
+ *
4
+ * Computes reflectance, transmittance and absorptance for a stack of absorbing,
5
+ * dispersive layers at arbitrary angle of incidence, in s and p polarization,
6
+ * together with exact analytic derivatives: the thickness Jacobian, the
7
+ * thickness Hessian, and the needle-insertion P-function.
8
+ *
9
+ * Conventions:
10
+ * ñ = n + ik k > 0 for absorbing media
11
+ * time factor exp(−iωt) a wave exp(i(kz − ωt)) decays for k > 0
12
+ * off-diagonals of the characteristic matrix carry −i
13
+ * wavelengths and thicknesses in nm, angles in degrees from normal
14
+ *
15
+ * All arithmetic is double precision. The JavaScript implementation shipped
16
+ * alongside this file is the behavioural reference; the two agree to float64
17
+ * round-off (see tests/).
18
+ *
19
+ * Builds with any C99 compiler; no dependencies beyond libm.
20
+ *
21
+ * References:
22
+ * • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125
23
+ * • Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996), Eqs. (3)–(6)
24
+ * • Tikhonravov, Trubetskov & DeBell, Appl. Opt. 35, 5493 (1996)
25
+ */
26
+
27
+ #include <math.h>
28
+ #include <stdlib.h>
29
+
30
+ /* WebAssembly builds export by name; native builds need no annotation. */
31
+ #if defined(__wasm__) || defined(__EMSCRIPTEN__)
32
+ #define TMM_EXPORT(name) __attribute__((export_name(name)))
33
+ #else
34
+ #define TMM_EXPORT(name)
35
+ #endif
36
+
37
+ #define PI 3.14159265358979323846
38
+ #define MAX_IM_DELTA 50.0
39
+ #define MATRIX_RESCALE_THRESHOLD 1e100
40
+
41
+ /* ── Complex number arithmetic (cx = {re, im}) ───────────────────────────────
42
+ * Mirrors thinFilmMath.js cadd/csub/cmul/cdiv/cabs2/cconj/csqrt/ccos/csin. */
43
+
44
+ typedef struct { double re, im; } cx;
45
+
46
+ static inline cx cmk(double re, double im) { cx z; z.re = re; z.im = im; return z; }
47
+
48
+ static inline cx cadd(cx a, cx b) { return cmk(a.re + b.re, a.im + b.im); }
49
+ static inline cx csub(cx a, cx b) { return cmk(a.re - b.re, a.im - b.im); }
50
+ static inline cx cmul(cx a, cx b) {
51
+ return cmk(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re);
52
+ }
53
+ static inline cx cdiv(cx a, cx b) {
54
+ double d = b.re * b.re + b.im * b.im;
55
+ return cmk((a.re * b.re + a.im * b.im) / d, (a.im * b.re - a.re * b.im) / d);
56
+ }
57
+ static inline double cabs2(cx a) { return a.re * a.re + a.im * a.im; }
58
+ static inline cx cconj(cx a) { return cmk(a.re, -a.im); }
59
+ static inline cx csqrt_(cx a) {
60
+ /* r = sqrt(sqrt(re²+im²)); theta = atan2(im,re)/2 — JS csqrt verbatim */
61
+ double r = sqrt(sqrt(a.re * a.re + a.im * a.im));
62
+ double theta = atan2(a.im, a.re) / 2.0;
63
+ return cmk(r * cos(theta), r * sin(theta));
64
+ }
65
+ static inline cx ccos_(cx a) {
66
+ return cmk(cos(a.re) * cosh(a.im), -sin(a.re) * sinh(a.im));
67
+ }
68
+ static inline cx csin_(cx a) {
69
+ return cmk(sin(a.re) * cosh(a.im), cos(a.re) * sinh(a.im));
70
+ }
71
+
72
+ /* ── 2×2 complex matrix [[a,b],[c,d]] and 2-vector {x,y} ──────────────────── */
73
+
74
+ typedef struct { cx a, b, c, d; } mat2;
75
+ typedef struct { cx x, y; } vec2;
76
+
77
+ static inline mat2 matmul(mat2 A, mat2 B) {
78
+ mat2 M;
79
+ M.a = cadd(cmul(A.a, B.a), cmul(A.b, B.c));
80
+ M.b = cadd(cmul(A.a, B.b), cmul(A.b, B.d));
81
+ M.c = cadd(cmul(A.c, B.a), cmul(A.d, B.c));
82
+ M.d = cadd(cmul(A.c, B.b), cmul(A.d, B.d));
83
+ return M;
84
+ }
85
+ /* Common scaling cancels from reflectance (Macleod Eq. 2.123, p. 45);
86
+ * tmm_core retains the scale required by transmittance (Eq. 2.125). */
87
+ static inline double rescaleMatrix(mat2 *M) {
88
+ double scale = 0.0;
89
+ scale = fmax(scale, fmax(fabs(M->a.re), fabs(M->a.im)));
90
+ scale = fmax(scale, fmax(fabs(M->b.re), fabs(M->b.im)));
91
+ scale = fmax(scale, fmax(fabs(M->c.re), fabs(M->c.im)));
92
+ scale = fmax(scale, fmax(fabs(M->d.re), fabs(M->d.im)));
93
+ if (scale <= MATRIX_RESCALE_THRESHOLD) return 0.0;
94
+ double inverse = 1.0 / scale;
95
+ M->a.re *= inverse; M->a.im *= inverse;
96
+ M->b.re *= inverse; M->b.im *= inverse;
97
+ M->c.re *= inverse; M->c.im *= inverse;
98
+ M->d.re *= inverse; M->d.im *= inverse;
99
+ return log(scale);
100
+ }
101
+ static inline vec2 cmatvec(mat2 M, vec2 v) {
102
+ vec2 o;
103
+ o.x = cadd(cmul(M.a, v.x), cmul(M.b, v.y));
104
+ o.y = cadd(cmul(M.c, v.x), cmul(M.d, v.y));
105
+ return o;
106
+ }
107
+
108
+ /* ── Snell's law: cosθ_j from incident (n0, sinθ0) into medium nj ─────────── */
109
+
110
+ static inline cx snellCosTheta(cx n0, cx sinTheta0, cx nj) {
111
+ cx sinThetaJ = cdiv(cmul(n0, sinTheta0), nj);
112
+ return csqrt_(csub(cmk(1.0, 0.0), cmul(sinThetaJ, sinThetaJ)));
113
+ }
114
+
115
+ /* ── Layer characteristic matrix (pol: 0 = s, 1 = p) ─────────────────────── */
116
+
117
+ static inline mat2 layerMatrix(cx nj, double dj_nm, double lambda_nm, cx cosTheta_j, int pol) {
118
+ double k0 = (2.0 * PI) / lambda_nm;
119
+ cx delta = cmul(cmul(nj, cmk(k0 * dj_nm, 0.0)), cosTheta_j);
120
+ if (delta.im > MAX_IM_DELTA) delta.im = MAX_IM_DELTA;
121
+ else if (delta.im < -MAX_IM_DELTA) delta.im = -MAX_IM_DELTA;
122
+ cx cosD = ccos_(delta);
123
+ cx sinD = csin_(delta);
124
+
125
+ cx eta = (pol == 0) ? cmul(nj, cosTheta_j) /* s: n cosθ */
126
+ : cdiv(nj, cosTheta_j); /* p: n / cosθ */
127
+
128
+ cx negI = cmk(0.0, -1.0);
129
+ cx iSinD_div_eta = cmul(negI, cdiv(sinD, eta));
130
+ cx iEta_sinD = cmul(negI, cmul(eta, sinD));
131
+
132
+ mat2 M;
133
+ M.a = cosD; M.b = iSinD_div_eta;
134
+ M.c = iEta_sinD; M.d = cosD;
135
+ return M;
136
+ }
137
+
138
+ /* ── Core TMM for one wavelength / polarization ──────────────────────────────
139
+ * Faithful port of tmm() in thinFilmMath.js. `layers` is N triples
140
+ * [n_re, n_im, d_nm]. Writes R,T,A to out[0..2]. (Zero-thickness layers skipped,
141
+ * exactly like the JS reference.) */
142
+
143
+ static void tmm_core(double lambda_nm, double theta_deg, int pol,
144
+ cx n0, cx ns, const double *layers, int N,
145
+ double *outR, double *outT, double *outA) {
146
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
147
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
148
+
149
+ cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
150
+
151
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
152
+ cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
153
+
154
+ /* identity */
155
+ mat2 M;
156
+ M.a = cmk(1.0, 0.0); M.b = cmk(0.0, 0.0);
157
+ M.c = cmk(0.0, 0.0); M.d = cmk(1.0, 0.0);
158
+ double logScale = 0.0;
159
+
160
+ for (int i = 0; i < N; i++) {
161
+ cx n = cmk(layers[3 * i + 0], layers[3 * i + 1]);
162
+ double d = layers[3 * i + 2];
163
+ if (d <= 0.0) continue;
164
+ cx cosThetaJ = snellCosTheta(n0, sinTheta0, n);
165
+ mat2 Mj = layerMatrix(n, d, lambda_nm, cosThetaJ, pol);
166
+ M = matmul(M, Mj);
167
+ logScale += rescaleMatrix(&M);
168
+ }
169
+
170
+ cx B = cadd(M.a, cmul(M.b, etaS));
171
+ cx C = cadd(M.c, cmul(M.d, etaS));
172
+ cx eta0B = cmul(eta0, B);
173
+ cx r = cdiv(csub(eta0B, C), cadd(eta0B, C));
174
+ cx t = cdiv(cmul(cmk(2.0, 0.0), eta0), cadd(eta0B, C));
175
+
176
+ double R = cabs2(r);
177
+ double T = etaS.re / eta0.re * cabs2(t) * exp(-2.0 * logScale);
178
+ if (T < 0.0) T = 0.0;
179
+ double A = 1.0 - R - T;
180
+ if (A < 0.0) A = 0.0;
181
+
182
+ *outR = R; *outT = T; *outA = A;
183
+ }
184
+
185
+ /* ── Exported: single (λ, θ, pol) evaluation ─────────────────────────────────
186
+ * out[0..2] = R, T, A. pol: 0 = s, 1 = p. Drop-in for tmm() in thinFilmMath.js. */
187
+
188
+ TMM_EXPORT("tmm_one")
189
+ void tmm_one(double lambda_nm, double theta_deg, int pol,
190
+ double n0_re, double n0_im, double ns_re, double ns_im,
191
+ const double *layers, int N, double *out) {
192
+ tmm_core(lambda_nm, theta_deg, pol,
193
+ cmk(n0_re, n0_im), cmk(ns_re, ns_im), layers, N,
194
+ &out[0], &out[1], &out[2]);
195
+ }
196
+
197
+ /* ── Exported: batched spectrum over a wavelength grid ───────────────────────
198
+ * One WASM call evaluates the full stack across all `nLam` wavelengths for BOTH
199
+ * polarizations, amortizing the JS↔WASM boundary (the actual speed win). This
200
+ * is the batched analogue of looping tmmAvg() in evaluateSpectrum().
201
+ *
202
+ * Memory layout (all f64, caller-owned, written from JS):
203
+ * lambdas : nLam wavelengths (nm)
204
+ * n0arr : nLam × 2 [re, im] incident-medium ñ per λ
205
+ * nsarr : nLam × 2 [re, im] substrate ñ per λ
206
+ * matNK : N × nLam × 2 per-layer ñ per λ, layout [layer][λ][re,im]
207
+ * thick : N layer thicknesses (nm)
208
+ * Outputs (each nLam):
209
+ * outRs,outTs,outAs (s-pol) outRp,outTp,outAp (p-pol)
210
+ * Caller forms avg = (s+p)/2 and selects the requested polarization in JS,
211
+ * exactly as tmmAvg() does, so the JS-side semantics are unchanged. */
212
+
213
+ TMM_EXPORT("tmm_spectrum")
214
+ void tmm_spectrum(const double *lambdas, int nLam,
215
+ const double *n0arr, const double *nsarr,
216
+ const double *matNK, const double *thick, int N,
217
+ double theta_deg,
218
+ double *outRs, double *outTs, double *outAs,
219
+ double *outRp, double *outTp, double *outAp) {
220
+ /* Reusable per-λ layer scratch [n_re, n_im, d] × N. */
221
+ double *layers = (N > 0) ? (double *)malloc(sizeof(double) * 3 * N) : NULL;
222
+
223
+ for (int li = 0; li < nLam; li++) {
224
+ double lam = lambdas[li];
225
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
226
+ cx ns = cmk(nsarr[2 * li + 0], nsarr[2 * li + 1]);
227
+
228
+ for (int k = 0; k < N; k++) {
229
+ /* matNK[k][li][.] */
230
+ long base = ((long)k * nLam + li) * 2;
231
+ layers[3 * k + 0] = matNK[base + 0];
232
+ layers[3 * k + 1] = matNK[base + 1];
233
+ layers[3 * k + 2] = thick[k];
234
+ }
235
+
236
+ tmm_core(lam, theta_deg, 0, n0, ns, layers, N, &outRs[li], &outTs[li], &outAs[li]);
237
+ tmm_core(lam, theta_deg, 1, n0, ns, layers, N, &outRp[li], &outTp[li], &outAp[li]);
238
+ }
239
+
240
+ if (layers) free(layers);
241
+ }
242
+
243
+ /* ── Exported: analytic thickness Jacobian for one (λ, θ, pol) ────────────────
244
+ * Faithful port of tmmThicknessJacobian() in thinFilmMath.js. Returns the exact
245
+ * analytic dR/dd_k, dT/dd_k, dA/dd_k for every layer at one sample — the DLS
246
+ * refiner's per-step gradient (2·N fewer evals than central differences).
247
+ *
248
+ * `layers` is N triples [n_re, n_im, d]; layers used AS-IS (no d>0 filter) for
249
+ * index parity with the caller's design, exactly like the JS reference.
250
+ * Outputs (each length N): dRdd, dTdd, dAdd. Also writes base R,T,A to base[0..2]. */
251
+
252
+ TMM_EXPORT("tmm_jacobian")
253
+ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
254
+ double n0_re, double n0_im, double ns_re, double ns_im,
255
+ const double *layers, int N,
256
+ double *dRdd, double *dTdd, double *dAdd, double *base) {
257
+ cx n0 = cmk(n0_re, n0_im);
258
+ cx ns = cmk(ns_re, ns_im);
259
+
260
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
261
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
262
+ cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
263
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
264
+ cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
265
+
266
+ /* Per-layer cosθ and characteristic matrices. */
267
+ cx *cosThJ = (cx *) malloc(sizeof(cx) * (N > 0 ? N : 1));
268
+ mat2 *Ms = (mat2 *)malloc(sizeof(mat2) * (N > 0 ? N : 1));
269
+ for (int k = 0; k < N; k++) {
270
+ cx n = cmk(layers[3 * k + 0], layers[3 * k + 1]);
271
+ double d = layers[3 * k + 2];
272
+ if (d < 0.0) d = 0.0;
273
+ cosThJ[k] = snellCosTheta(n0, sinTheta0, n);
274
+ Ms[k] = layerMatrix(n, d, lambda_nm, cosThJ[k], pol);
275
+ }
276
+
277
+ /* Pre[j] = M_0·…·M_{j-1}; Post[j] = M_j·…·M_{N-1}·[1, ηs]. */
278
+ mat2 *Pre = (mat2 *)malloc(sizeof(mat2) * (N + 1));
279
+ vec2 *Post = (vec2 *)malloc(sizeof(vec2) * (N + 1));
280
+ Pre[0].a = cmk(1.0, 0.0); Pre[0].b = cmk(0.0, 0.0);
281
+ Pre[0].c = cmk(0.0, 0.0); Pre[0].d = cmk(1.0, 0.0);
282
+ for (int j = 0; j < N; j++) Pre[j + 1] = matmul(Pre[j], Ms[j]);
283
+ Post[N].x = cmk(1.0, 0.0); Post[N].y = etaS;
284
+ for (int j = N - 1; j >= 0; j--) Post[j] = cmatvec(Ms[j], Post[j + 1]);
285
+
286
+ cx Bv = Post[0].x, Cv = Post[0].y;
287
+ cx den = cadd(cmul(eta0, Bv), Cv);
288
+ cx den2 = cmul(den, den);
289
+ cx r = cdiv(csub(cmul(eta0, Bv), Cv), den);
290
+ cx t = cdiv(cmul(cmk(2.0, 0.0), eta0), den);
291
+ double R = cabs2(r);
292
+ double Tfac = etaS.re / eta0.re;
293
+ double T = Tfac * cabs2(t); if (T < 0.0) T = 0.0;
294
+ double A = 1.0 - R - T; if (A < 0.0) A = 0.0;
295
+ base[0] = R; base[1] = T; base[2] = A;
296
+
297
+ cx f = cdiv(cmul(cmk(2.0, 0.0), eta0), den2);
298
+ double k0 = (2.0 * PI) / lambda_nm;
299
+
300
+ for (int k = 0; k < N; k++) {
301
+ cx n = cmk(layers[3 * k + 0], layers[3 * k + 1]);
302
+ double d = layers[3 * k + 2]; if (d < 0.0) d = 0.0;
303
+ cx cth = cosThJ[k];
304
+ cx etaK = (pol == 0) ? cmul(n, cth) : cdiv(n, cth);
305
+ cx Q = cmul(cmul(n, cmk(k0, 0.0)), cth); /* (2π/λ) n cosθ */
306
+ cx delta = cmul(cmul(n, cmk(k0 * d, 0.0)), cth);
307
+ cx cD = ccos_(delta), sD = csin_(delta);
308
+
309
+ /* dM_k/dd_k = Q · [[ −sinδ, −i cosδ/η ], [ −i η cosδ, −sinδ ]] */
310
+ cx negI = cmk(0.0, -1.0), neg1 = cmk(-1.0, 0.0);
311
+ mat2 dMk;
312
+ dMk.a = cmul(Q, cmul(neg1, sD));
313
+ dMk.b = cmul(Q, cmul(negI, cdiv(cD, etaK)));
314
+ dMk.c = cmul(Q, cmul(negI, cmul(etaK, cD)));
315
+ dMk.d = cmul(Q, cmul(neg1, sD));
316
+
317
+ vec2 dV = cmatvec(Pre[k], cmatvec(dMk, Post[k + 1]));
318
+ cx dB = dV.x, dC = dV.y;
319
+
320
+ /* metrics(dB,dC) — verbatim from validated tmmNeedleScan.metrics */
321
+ cx dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
322
+ double dR = 2.0 * (cmul(cconj(r), dr)).re;
323
+ cx dt = cmul(neg1, cmul(f, cadd(cmul(eta0, dB), dC)));
324
+ double dT = Tfac * 2.0 * (cmul(cconj(t), dt)).re;
325
+ dRdd[k] = dR; dTdd[k] = dT; dAdd[k] = -(dR + dT);
326
+ }
327
+
328
+ free(cosThJ); free(Ms); free(Pre); free(Post);
329
+ }
330
+
331
+ /* ── Analytic needle P-function scan ─────────────────────────────────────────
332
+ * Faithful port of tmmNeedleScan() in thinFilmMath.js — the d→0 limit of
333
+ * Sullivan's pre/post method (Tikhonravov's analytic P-function). Returns the
334
+ * merit-gradient ingredients {dR,dT,dA} of inserting an infinitesimal needle of
335
+ * each candidate index at every gap position (0..N) and, optionally, at intra-
336
+ * layer split fractions. The {dR,dT,dA} chain rule is identical to tmm_jacobian.
337
+ *
338
+ * `layers` used AS-IS (no d>0 filter), index parity with the caller's stack.
339
+ * Outputs (caller-owned f64):
340
+ * base : 3 [R,T,A]
341
+ * gaps : (N+1)*nCand*3 layout [pos][cand][dR,dT,dA]
342
+ * intra : N*nFrac*nCand*3 layout [layer][frac][cand][dR,dT,dA] (nFrac>0)
343
+ */
344
+
345
+ /* {dR,dT,dA} from d[B,C]/dd — verbatim from tmm_jacobian.metrics. */
346
+ static void needle_metrics(cx Bv, cx Cv, cx eta0, cx f, cx r, cx t, double Tfac,
347
+ cx dB, cx dC, double *o) {
348
+ cx dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
349
+ double dR = 2.0 * (cmul(cconj(r), dr)).re;
350
+ cx dt = cmul(cmk(-1.0, 0.0), cmul(f, cadd(cmul(eta0, dB), dC)));
351
+ double dT = Tfac * 2.0 * (cmul(cconj(t), dt)).re;
352
+ o[0] = dR; o[1] = dT; o[2] = -(dR + dT);
353
+ }
354
+
355
+ TMM_EXPORT("tmm_needle_scan")
356
+ void tmm_needle_scan(double lambda_nm, double theta_deg, int pol,
357
+ double n0_re, double n0_im, double ns_re, double ns_im,
358
+ const double *layers, int N,
359
+ const double *candNs, int nCand,
360
+ const double *fracs, int nFrac,
361
+ double *base, double *gaps, double *intra) {
362
+ cx n0 = cmk(n0_re, n0_im), ns = cmk(ns_re, ns_im);
363
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
364
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
365
+ cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
366
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
367
+ cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
368
+
369
+ double k0 = (2.0 * PI) / lambda_nm;
370
+
371
+ cx *cosThJ = (cx *) malloc(sizeof(cx) * (N > 0 ? N : 1));
372
+ mat2 *Ms = (mat2 *)malloc(sizeof(mat2) * (N > 0 ? N : 1));
373
+ for (int k = 0; k < N; k++) {
374
+ cx n = cmk(layers[3 * k + 0], layers[3 * k + 1]);
375
+ double d = layers[3 * k + 2]; if (d < 0.0) d = 0.0;
376
+ cosThJ[k] = snellCosTheta(n0, sinTheta0, n);
377
+ Ms[k] = layerMatrix(n, d, lambda_nm, cosThJ[k], pol);
378
+ }
379
+
380
+ mat2 *Pre = (mat2 *)malloc(sizeof(mat2) * (N + 1));
381
+ vec2 *Post = (vec2 *)malloc(sizeof(vec2) * (N + 1));
382
+ Pre[0].a = cmk(1.0, 0.0); Pre[0].b = cmk(0.0, 0.0);
383
+ Pre[0].c = cmk(0.0, 0.0); Pre[0].d = cmk(1.0, 0.0);
384
+ for (int j = 0; j < N; j++) Pre[j + 1] = matmul(Pre[j], Ms[j]);
385
+ Post[N].x = cmk(1.0, 0.0); Post[N].y = etaS;
386
+ for (int j = N - 1; j >= 0; j--) Post[j] = cmatvec(Ms[j], Post[j + 1]);
387
+
388
+ cx Bv = Post[0].x, Cv = Post[0].y;
389
+ cx den = cadd(cmul(eta0, Bv), Cv);
390
+ cx den2 = cmul(den, den);
391
+ cx r = cdiv(csub(cmul(eta0, Bv), Cv), den);
392
+ cx t = cdiv(cmul(cmk(2.0, 0.0), eta0), den);
393
+ double R = cabs2(r);
394
+ double Tfac = etaS.re / eta0.re;
395
+ double T = Tfac * cabs2(t); if (T < 0.0) T = 0.0;
396
+ double A = 1.0 - R - T; if (A < 0.0) A = 0.0;
397
+ base[0] = R; base[1] = T; base[2] = A;
398
+
399
+ cx f = cdiv(cmul(cmk(2.0, 0.0), eta0), den2);
400
+
401
+ /* Needle derivative-matrix A per candidate: A = [[0,−iQ/ηₐ],[−iηₐQ,0]]. */
402
+ mat2 *Ac = (mat2 *)malloc(sizeof(mat2) * (nCand > 0 ? nCand : 1));
403
+ for (int c = 0; c < nCand; c++) {
404
+ cx nA = cmk(candNs[2 * c + 0], candNs[2 * c + 1]);
405
+ cx cthA = snellCosTheta(n0, sinTheta0, nA);
406
+ cx etaA = (pol == 0) ? cmul(nA, cthA) : cdiv(nA, cthA);
407
+ cx Q = cmul(cmul(nA, cmk(k0, 0.0)), cthA);
408
+ cx negI = cmk(0.0, -1.0);
409
+ mat2 Am;
410
+ Am.a = cmk(0.0, 0.0); Am.b = cmul(negI, cdiv(Q, etaA));
411
+ Am.c = cmul(negI, cmul(etaA, Q)); Am.d = cmk(0.0, 0.0);
412
+ Ac[c] = Am;
413
+ }
414
+
415
+ /* Gaps: pos = 0..N, every candidate. */
416
+ for (int pos = 0; pos <= N; pos++) {
417
+ vec2 post = Post[pos];
418
+ mat2 pre = Pre[pos];
419
+ for (int c = 0; c < nCand; c++) {
420
+ vec2 dV = cmatvec(pre, cmatvec(Ac[c], post));
421
+ needle_metrics(Bv, Cv, eta0, f, r, t, Tfac, dV.x, dV.y,
422
+ &gaps[((long)pos * nCand + c) * 3]);
423
+ }
424
+ }
425
+
426
+ /* Intra-layer splits (host-split), if requested. */
427
+ if (nFrac > 0) {
428
+ for (int k = 0; k < N; k++) {
429
+ cx n = cmk(layers[3 * k + 0], layers[3 * k + 1]);
430
+ double d = layers[3 * k + 2]; /* raw d (matches JS intra) */
431
+ cx cth = cosThJ[k];
432
+ for (int fi = 0; fi < nFrac; fi++) {
433
+ double frac = fracs[fi];
434
+ double dl = frac * d; if (dl < 1e-9) dl = 1e-9;
435
+ double dr_ = (1.0 - frac) * d; if (dr_ < 1e-9) dr_ = 1e-9;
436
+ mat2 Mleft = layerMatrix(n, dl, lambda_nm, cth, pol);
437
+ mat2 Mright = layerMatrix(n, dr_, lambda_nm, cth, pol);
438
+ mat2 preIn = matmul(Pre[k], Mleft);
439
+ vec2 postIn = cmatvec(Mright, Post[k + 1]);
440
+ for (int c = 0; c < nCand; c++) {
441
+ vec2 dV = cmatvec(preIn, cmatvec(Ac[c], postIn));
442
+ long off = (((long)k * nFrac + fi) * nCand + c) * 3;
443
+ needle_metrics(Bv, Cv, eta0, f, r, t, Tfac, dV.x, dV.y, &intra[off]);
444
+ }
445
+ }
446
+ }
447
+ }
448
+
449
+ free(cosThJ); free(Ms); free(Pre); free(Post); free(Ac);
450
+ }
451
+
452
+ /* ── Analytic thickness-Hessian kernel ───────────
453
+ * LINE-BY-LINE port of tmmThicknessHessian() in thinFilmMath.js — the EXACT
454
+ * analytic second derivatives ∂²{R,T,A}/∂dᵢ∂dⱼ (full N×N symmetric) plus the
455
+ * first derivatives, at one (λ,θ,pol). Used by the bounded-SQP / Newton inner
456
+ * refiner; the JS remains the oracle (tests/wasm_hessian_equivalence.mjs).
457
+ *
458
+ * Outputs (caller-owned f64):
459
+ * base : 3 [R,T,A]
460
+ * dRdd/dTdd/dAdd : N each first derivatives (verbatim tmm_jacobian)
461
+ * d2Rdd/d2Tdd/d2Add : N*N each row-major, FULL symmetric (both triangles set)
462
+ */
463
+ TMM_EXPORT("tmm_hessian")
464
+ void tmm_hessian(double lambda_nm, double theta_deg, int pol,
465
+ double n0_re, double n0_im, double ns_re, double ns_im,
466
+ const double *layers, int N,
467
+ double *dRdd, double *dTdd, double *dAdd,
468
+ double *d2Rdd, double *d2Tdd, double *d2Add, double *base) {
469
+ cx n0 = cmk(n0_re, n0_im);
470
+ cx ns = cmk(ns_re, ns_im);
471
+
472
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
473
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
474
+ cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
475
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
476
+ cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
477
+
478
+ cx *cosThJ = (cx *) malloc(sizeof(cx) * (N > 0 ? N : 1));
479
+ mat2 *Ms = (mat2 *)malloc(sizeof(mat2) * (N > 0 ? N : 1));
480
+ for (int k = 0; k < N; k++) {
481
+ cx n = cmk(layers[3 * k + 0], layers[3 * k + 1]);
482
+ double d = layers[3 * k + 2]; if (d < 0.0) d = 0.0;
483
+ cosThJ[k] = snellCosTheta(n0, sinTheta0, n);
484
+ Ms[k] = layerMatrix(n, d, lambda_nm, cosThJ[k], pol);
485
+ }
486
+
487
+ mat2 *Pre = (mat2 *)malloc(sizeof(mat2) * (N + 1));
488
+ vec2 *Post = (vec2 *)malloc(sizeof(vec2) * (N + 1));
489
+ Pre[0].a = cmk(1.0, 0.0); Pre[0].b = cmk(0.0, 0.0);
490
+ Pre[0].c = cmk(0.0, 0.0); Pre[0].d = cmk(1.0, 0.0);
491
+ for (int j = 0; j < N; j++) Pre[j + 1] = matmul(Pre[j], Ms[j]);
492
+ Post[N].x = cmk(1.0, 0.0); Post[N].y = etaS;
493
+ for (int j = N - 1; j >= 0; j--) Post[j] = cmatvec(Ms[j], Post[j + 1]);
494
+
495
+ cx Bv = Post[0].x, Cv = Post[0].y;
496
+ cx den = cadd(cmul(eta0, Bv), Cv);
497
+ cx den2 = cmul(den, den);
498
+ cx den3 = cmul(den2, den);
499
+ cx r = cdiv(csub(cmul(eta0, Bv), Cv), den);
500
+ cx t = cdiv(cmul(cmk(2.0, 0.0), eta0), den);
501
+ double R = cabs2(r);
502
+ double Tfac = etaS.re / eta0.re;
503
+ double T = Tfac * cabs2(t); if (T < 0.0) T = 0.0;
504
+ double A = 1.0 - R - T; if (A < 0.0) A = 0.0;
505
+ base[0] = R; base[1] = T; base[2] = A;
506
+ cx f = cdiv(cmul(cmk(2.0, 0.0), eta0), den2);
507
+
508
+ double k0 = (2.0 * PI) / lambda_nm;
509
+
510
+ /* Per-layer first-derivative pieces: dM[k], d2M[k], v[k]=dMₖ·Post[k+1],
511
+ * [dB,dC] and the first-derivative metrics. */
512
+ mat2 *dM = (mat2 *)malloc(sizeof(mat2) * (N > 0 ? N : 1));
513
+ mat2 *d2M = (mat2 *)malloc(sizeof(mat2) * (N > 0 ? N : 1));
514
+ vec2 *v = (vec2 *)malloc(sizeof(vec2) * (N > 0 ? N : 1));
515
+ cx *dBa = (cx *) malloc(sizeof(cx) * (N > 0 ? N : 1));
516
+ cx *dCa = (cx *) malloc(sizeof(cx) * (N > 0 ? N : 1));
517
+ cx negI = cmk(0.0, -1.0), posI = cmk(0.0, 1.0), neg1 = cmk(-1.0, 0.0);
518
+ for (int k = 0; k < N; k++) {
519
+ cx n = cmk(layers[3 * k + 0], layers[3 * k + 1]);
520
+ double d = layers[3 * k + 2]; if (d < 0.0) d = 0.0;
521
+ cx cth = cosThJ[k];
522
+ cx etaK = (pol == 0) ? cmul(n, cth) : cdiv(n, cth);
523
+ cx Q = cmul(cmul(n, cmk(k0, 0.0)), cth); /* (2π/λ) n cosθ */
524
+ cx Q2 = cmul(Q, Q);
525
+ cx delta = cmul(cmul(n, cmk(k0 * d, 0.0)), cth);
526
+ cx cD = ccos_(delta), sD = csin_(delta);
527
+ /* dMₖ/ddₖ = Q·[[ −sinδ, −i cosδ/η ], [ −i η cosδ, −sinδ ]] */
528
+ dM[k].a = cmul(Q, cmul(neg1, sD));
529
+ dM[k].b = cmul(Q, cmul(negI, cdiv(cD, etaK)));
530
+ dM[k].c = cmul(Q, cmul(negI, cmul(etaK, cD)));
531
+ dM[k].d = cmul(Q, cmul(neg1, sD));
532
+ /* d²Mₖ/ddₖ² = Q²·[[ −cosδ, i sinδ/η ], [ i η sinδ, −cosδ ]] */
533
+ d2M[k].a = cmul(Q2, cmul(neg1, cD));
534
+ d2M[k].b = cmul(Q2, cmul(posI, cdiv(sD, etaK)));
535
+ d2M[k].c = cmul(Q2, cmul(posI, cmul(etaK, sD)));
536
+ d2M[k].d = cmul(Q2, cmul(neg1, cD));
537
+ v[k] = cmatvec(dM[k], Post[k + 1]);
538
+ vec2 dVk = cmatvec(Pre[k], v[k]);
539
+ dBa[k] = dVk.x; dCa[k] = dVk.y;
540
+ cx dr = cmul(f, csub(cmul(Cv, dBa[k]), cmul(Bv, dCa[k])));
541
+ double dR = 2.0 * (cmul(cconj(r), dr)).re;
542
+ cx dt = cmul(neg1, cmul(f, cadd(cmul(eta0, dBa[k]), dCa[k])));
543
+ double dT = Tfac * 2.0 * (cmul(cconj(t), dt)).re;
544
+ dRdd[k] = dR; dTdd[k] = dT; dAdd[k] = -(dR + dT);
545
+ }
546
+
547
+ mat2 I; I.a = cmk(1.0, 0.0); I.b = cmk(0.0, 0.0); I.c = cmk(0.0, 0.0); I.d = cmk(1.0, 0.0);
548
+ for (int i = 0; i < N; i++) {
549
+ mat2 Wmat_i = matmul(Pre[i], dM[i]); /* Pre[i]·dMᵢ (used for j>i) */
550
+ mat2 Cmid = I; /* M_{i+1}···M_{j-1}, empty at j=i+1 */
551
+ for (int j = i; j < N; j++) {
552
+ cx d2Bv, d2Cv;
553
+ if (j == i) {
554
+ vec2 w = cmatvec(Pre[i], cmatvec(d2M[i], Post[i + 1]));
555
+ d2Bv = w.x; d2Cv = w.y;
556
+ } else {
557
+ vec2 w = cmatvec(Wmat_i, cmatvec(Cmid, v[j]));
558
+ d2Bv = w.x; d2Cv = w.y;
559
+ }
560
+ cx dBi = dBa[i], dCi = dCa[i], dBj = dBa[j], dCj = dCa[j];
561
+ cx dr_i = cmul(f, csub(cmul(Cv, dBi), cmul(Bv, dCi)));
562
+ cx dr_j = cmul(f, csub(cmul(Cv, dBj), cmul(Bv, dCj)));
563
+ cx dden_i = cadd(cmul(eta0, dBi), dCi);
564
+ cx dden_j = cadd(cmul(eta0, dBj), dCj);
565
+ /* d²r_ij = f(dCᵢdBⱼ + C d²B − dBᵢdCⱼ − B d²C) − 2 drⱼ ddenᵢ/den */
566
+ cx innerR = csub(
567
+ cadd(cmul(dCi, dBj), cmul(Cv, d2Bv)),
568
+ cadd(cmul(dBi, dCj), cmul(Bv, d2Cv)));
569
+ cx d2r = csub(cmul(f, innerR),
570
+ cdiv(cmul(cmul(cmk(2.0, 0.0), dr_j), dden_i), den));
571
+ double d2Rij = 2.0 * ((cmul(cconj(dr_i), dr_j)).re + (cmul(cconj(r), d2r)).re);
572
+ cx dt_i = cmul(neg1, cmul(f, dden_i));
573
+ cx dt_j = cmul(neg1, cmul(f, dden_j));
574
+ cx d2den = cadd(cmul(eta0, d2Bv), d2Cv);
575
+ cx d2t = cadd(
576
+ cmul(cmul(cmk(-2.0, 0.0), eta0), cdiv(d2den, den2)),
577
+ cmul(cmul(cmk(4.0, 0.0), eta0), cdiv(cmul(dden_i, dden_j), den3)));
578
+ double d2Tij = Tfac * 2.0 * ((cmul(cconj(dt_i), dt_j)).re + (cmul(cconj(t), d2t)).re);
579
+ double d2Aij = -(d2Rij + d2Tij);
580
+ d2Rdd[(long)i * N + j] = d2Rdd[(long)j * N + i] = d2Rij;
581
+ d2Tdd[(long)i * N + j] = d2Tdd[(long)j * N + i] = d2Tij;
582
+ d2Add[(long)i * N + j] = d2Add[(long)j * N + i] = d2Aij;
583
+ if (j >= i + 1) Cmid = matmul(Cmid, Ms[j]); /* advance middle: include M_j */
584
+ }
585
+ }
586
+
587
+ free(cosThJ); free(Ms); free(Pre); free(Post);
588
+ free(dM); free(d2M); free(v); free(dBa); free(dCa);
589
+ }
Binary file