tmmcore 0.1.0 → 0.3.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/src/tmm_kernel.c CHANGED
@@ -1,11 +1,15 @@
1
1
  /*
2
- * tmm_kernel.c transfer-matrix method for multilayer thin films.
2
+ * tmm_kernel.c : transfer-matrix method for multilayer thin films.
3
3
  *
4
4
  * Computes reflectance, transmittance and absorptance for a stack of absorbing,
5
5
  * dispersive layers at arbitrary angle of incidence, in s and p polarization,
6
6
  * together with exact analytic derivatives: the thickness Jacobian, the
7
7
  * thickness Hessian, and the needle-insertion P-function.
8
8
  *
9
+ * Also computes phase, group delay, group delay dispersion and third-order
10
+ * dispersion, by carrying the same characteristic matrix in third-order Taylor
11
+ * arithmetic with angular frequency as the differentiation variable.
12
+ *
9
13
  * Conventions:
10
14
  * ñ = n + ik k > 0 for absorbing media
11
15
  * time factor exp(−iωt) a wave exp(i(kz − ωt)) decays for k > 0
@@ -22,6 +26,7 @@
22
26
  * • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125
23
27
  * • Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996), Eqs. (3)–(6)
24
28
  * • Tikhonravov, Trubetskov & DeBell, Appl. Opt. 35, 5493 (1996)
29
+ * • Birge & Kärtner, Appl. Opt. 45, 1478 (2006) [phase dispersion]
25
30
  */
26
31
 
27
32
  #include <math.h>
@@ -57,7 +62,7 @@ static inline cx cdiv(cx a, cx b) {
57
62
  static inline double cabs2(cx a) { return a.re * a.re + a.im * a.im; }
58
63
  static inline cx cconj(cx a) { return cmk(a.re, -a.im); }
59
64
  static inline cx csqrt_(cx a) {
60
- /* r = sqrt(sqrt(re²+im²)); theta = atan2(im,re)/2 JS csqrt verbatim */
65
+ /* r = sqrt(sqrt(re²+im²)); theta = atan2(im,re)/2 : JS csqrt verbatim */
61
66
  double r = sqrt(sqrt(a.re * a.re + a.im * a.im));
62
67
  double theta = atan2(a.im, a.re) / 2.0;
63
68
  return cmk(r * cos(theta), r * sin(theta));
@@ -240,9 +245,339 @@ void tmm_spectrum(const double *lambdas, int nLam,
240
245
  if (layers) free(layers);
241
246
  }
242
247
 
248
+ /* ── Shared tails for the growing-stack kernels ──────────────────────────────
249
+ * Forward tail: reproduces the [B,C] → r,t → R,T block of tmm_core on an
250
+ * already-built characteristic matrix. A is not formed here; the incoherent
251
+ * slab combination that consumes these results computes it after the sum, and
252
+ * a semi-infinite caller forms 1−R−T itself.
253
+ *
254
+ * Reverse tail: reflectance of the same stack seen from the substrate side.
255
+ * The characteristic matrix of the reversed stack is the anti-transpose of the
256
+ * forward one (every layer matrix is invariant under anti-transposition), so
257
+ * the reverse pass costs no second product, and the common rescale factor
258
+ * cancels in the amplitude ratio. */
259
+
260
+ static inline void growingTailFwd(mat2 M, cx eta0, cx etaS, double logScale,
261
+ double *outR, double *outT) {
262
+ cx B = cadd(M.a, cmul(M.b, etaS));
263
+ cx C = cadd(M.c, cmul(M.d, etaS));
264
+ cx eta0B = cmul(eta0, B);
265
+ cx r = cdiv(csub(eta0B, C), cadd(eta0B, C));
266
+ cx t = cdiv(cmul(cmk(2.0, 0.0), eta0), cadd(eta0B, C));
267
+ double R = cabs2(r);
268
+ double T = etaS.re / eta0.re * cabs2(t) * exp(-2.0 * logScale);
269
+ if (T < 0.0) T = 0.0;
270
+ *outR = R; *outT = T;
271
+ }
272
+
273
+ static inline double growingTailRev(mat2 M, cx eta0, cx etaS) {
274
+ cx B = cadd(M.d, cmul(M.b, eta0));
275
+ cx C = cadd(M.c, cmul(M.a, eta0));
276
+ cx etaSB = cmul(etaS, B);
277
+ cx r = cdiv(csub(etaSB, C), cadd(etaSB, C));
278
+ return cabs2(r);
279
+ }
280
+
281
+ /* ── Exported: monitor curve of one growing layer ────────────────────────────
282
+ * The signal of one layer as it grows on a completed stack, at one wavelength,
283
+ * both polarizations: the incremental control algorithm of Tikhonravov &
284
+ * Trubetskov, Appl. Opt. 44, 6877 (2005). The completed stack's characteristic
285
+ * matrix is built once; each sample then costs one layer matrix, one 2×2
286
+ * multiply and the tails, so a sweep is linear in samples rather than in
287
+ * samples × stack depth.
288
+ *
289
+ * The growing layer faces the incident medium, so its matrix multiplies the
290
+ * completed product from the LEFT, exactly as createMonitorTmmEvaluator in the
291
+ * JS reference.
292
+ *
293
+ * base : NB triples [n_re, n_im, d_nm], the completed stack outermost first
294
+ * (zero-thickness entries skipped, as everywhere)
295
+ * ng : growing layer ñ at this wavelength
296
+ * dArr : nD sample thicknesses of the growing layer (nm); d ≤ 0 evaluates
297
+ * the bare completed stack
298
+ * Outputs (each nD):
299
+ * outRs/outTs, outRp/outTp : forward R and T of the coated surface
300
+ * outRrs/outRrp : its reflectance from the substrate side, for
301
+ * callers that model the witness as an incoherent
302
+ * slab with a bare back face */
303
+
304
+ TMM_EXPORT("tmm_monitor_curve")
305
+ void tmm_monitor_curve(double lambda_nm, double theta_deg,
306
+ double n0_re, double n0_im, double ns_re, double ns_im,
307
+ const double *base, int NB,
308
+ double ng_re, double ng_im,
309
+ const double *dArr, int nD,
310
+ double *outRs, double *outTs,
311
+ double *outRp, double *outTp,
312
+ double *outRrs, double *outRrp) {
313
+ cx n0 = cmk(n0_re, n0_im);
314
+ cx ns = cmk(ns_re, ns_im);
315
+ cx ng = cmk(ng_re, ng_im);
316
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
317
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
318
+
319
+ for (int pol = 0; pol < 2; pol++) {
320
+ cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
321
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
322
+ cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
323
+
324
+ mat2 Mb;
325
+ Mb.a = cmk(1.0, 0.0); Mb.b = cmk(0.0, 0.0);
326
+ Mb.c = cmk(0.0, 0.0); Mb.d = cmk(1.0, 0.0);
327
+ double logScaleB = 0.0;
328
+ for (int i = 0; i < NB; i++) {
329
+ cx n = cmk(base[3 * i + 0], base[3 * i + 1]);
330
+ double d = base[3 * i + 2];
331
+ if (d <= 0.0) continue;
332
+ cx cosThetaJ = snellCosTheta(n0, sinTheta0, n);
333
+ Mb = matmul(Mb, layerMatrix(n, d, lambda_nm, cosThetaJ, pol));
334
+ logScaleB += rescaleMatrix(&Mb);
335
+ }
336
+ cx cosThetaG = snellCosTheta(n0, sinTheta0, ng);
337
+
338
+ double *oR = pol ? outRp : outRs;
339
+ double *oT = pol ? outTp : outTs;
340
+ double *oRr = pol ? outRrp : outRrs;
341
+
342
+ for (int k = 0; k < nD; k++) {
343
+ mat2 M = Mb;
344
+ double logScale = logScaleB;
345
+ double d = dArr[k];
346
+ if (d > 0.0) {
347
+ M = matmul(layerMatrix(ng, d, lambda_nm, cosThetaG, pol), Mb);
348
+ logScale += rescaleMatrix(&M);
349
+ }
350
+ growingTailFwd(M, eta0, etaS, logScale, &oR[k], &oT[k]);
351
+ oRr[k] = growingTailRev(M, eta0, etaS);
352
+ }
353
+ }
354
+ }
355
+
356
+ /* ── Exported: spectra of a growing stack, one per deposited layer ───────────
357
+ * The front-surface passes of a deposition run in a single call: layers arrive
358
+ * in DEPOSITION order (first deposited, substrate-adjacent, first), each is
359
+ * folded into the running product from the left, since the newest layer faces
360
+ * the incident medium, and the spectrum after every fold is written out. The
361
+ * same incremental structure as tmm_monitor_curve over a wavelength grid, so
362
+ * all N step spectra together cost what the final one costs alone.
363
+ *
364
+ * Memory layout mirrors tmm_spectrum:
365
+ * matNK : N × nLam × 2, [layer][λ][re, im], deposition order
366
+ * thick : N thicknesses (nm); a step of zero thickness repeats the previous
367
+ * spectrum, matching the d ≤ 0 skip of the reference loop
368
+ * Outputs, each N × nLam, step-major ([step][λ]):
369
+ * outRs/outTs, outRp/outTp : forward R and T after each step
370
+ * outRrs/outRrp : reflectance from the substrate side after each
371
+ * step, for the incoherent slab combination */
372
+
373
+ /* Returns 1 on success; 0, with the outputs untouched, when the working
374
+ * state could not be allocated. */
375
+ TMM_EXPORT("tmm_deposition_spectra")
376
+ int tmm_deposition_spectra(const double *lambdas, int nLam,
377
+ const double *n0arr, const double *nsarr,
378
+ const double *matNK, const double *thick, int N,
379
+ double theta_deg,
380
+ double *outRs, double *outTs,
381
+ double *outRp, double *outTp,
382
+ double *outRrs, double *outRrp) {
383
+ /* Running product and admittances per (λ, pol); pol-major blocks. */
384
+ size_t cells = (size_t)nLam * 2;
385
+ mat2 *M = (mat2 *)malloc(sizeof(mat2) * cells);
386
+ double *logScale = (double *)malloc(sizeof(double) * cells);
387
+ cx *eta0v = (cx *)malloc(sizeof(cx) * cells);
388
+ cx *etaSv = (cx *)malloc(sizeof(cx) * cells);
389
+ if (!M || !logScale || !eta0v || !etaSv) {
390
+ free(M); free(logScale); free(eta0v); free(etaSv);
391
+ return 0;
392
+ }
393
+
394
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
395
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
396
+
397
+ for (int li = 0; li < nLam; li++) {
398
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
399
+ cx ns = cmk(nsarr[2 * li + 0], nsarr[2 * li + 1]);
400
+ for (int pol = 0; pol < 2; pol++) {
401
+ size_t at = (size_t)pol * nLam + li;
402
+ eta0v[at] = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
403
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
404
+ etaSv[at] = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
405
+ M[at].a = cmk(1.0, 0.0); M[at].b = cmk(0.0, 0.0);
406
+ M[at].c = cmk(0.0, 0.0); M[at].d = cmk(1.0, 0.0);
407
+ logScale[at] = 0.0;
408
+ }
409
+ }
410
+
411
+ for (int k = 0; k < N; k++) {
412
+ double d = thick[k];
413
+ for (int li = 0; li < nLam; li++) {
414
+ double lam = lambdas[li];
415
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
416
+ cx n = cmk(matNK[((size_t)k * nLam + li) * 2 + 0],
417
+ matNK[((size_t)k * nLam + li) * 2 + 1]);
418
+ for (int pol = 0; pol < 2; pol++) {
419
+ size_t at = (size_t)pol * nLam + li;
420
+ if (d > 0.0) {
421
+ cx cosThetaJ = snellCosTheta(n0, sinTheta0, n);
422
+ M[at] = matmul(layerMatrix(n, d, lam, cosThetaJ, pol), M[at]);
423
+ logScale[at] += rescaleMatrix(&M[at]);
424
+ }
425
+ size_t out = (size_t)k * nLam + li;
426
+ double *oR = pol ? outRp : outRs;
427
+ double *oT = pol ? outTp : outTs;
428
+ double *oRr = pol ? outRrp : outRrs;
429
+ growingTailFwd(M[at], eta0v[at], etaSv[at], logScale[at],
430
+ &oR[out], &oT[out]);
431
+ oRr[out] = growingTailRev(M[at], eta0v[at], etaSv[at]);
432
+ }
433
+ }
434
+ }
435
+
436
+ free(M); free(logScale); free(eta0v); free(etaSv);
437
+ return 1;
438
+ }
439
+
440
+ /* ── Exported: persistent growing-layer evaluator over a wavelength grid ─────
441
+ * The stateful counterpart of tmm_monitor_curve, batched the other way: one
442
+ * thickness of the growing layer per call, the WHOLE wavelength grid at once.
443
+ * This is the shape a broadband monitor scan needs, where every scan reads a
444
+ * full spectrum of the growing stack and the completed layers beneath do not
445
+ * change until the layer is cut.
446
+ *
447
+ * Lifecycle: create() folds the completed stack's characteristic matrices for
448
+ * every (λ, pol) once and keeps them; set_top() declares the growing layer's
449
+ * ñ(λ); sample() then answers one thickness across the grid, costing one layer
450
+ * matrix, one 2×2 multiply and the tails per (λ, pol). free() releases the
451
+ * state. Handles are opaque pointers; the caller owns their lifetime.
452
+ *
453
+ * Layout matches tmm_deposition_spectra: matNK is NB × nLam × 2 layer-major
454
+ * with the completed stack OUTERMOST FIRST (the fold is M = M · M_k, exactly
455
+ * the JS reference createMonitorTmmEvaluator); zero-thickness entries are
456
+ * skipped. Outputs of sample(), each nLam: forward R and T plus the
457
+ * substrate-side reflectance, for the incoherent slab combination. */
458
+
459
+ typedef struct {
460
+ int nLam;
461
+ cx sinTheta0;
462
+ int topSet;
463
+ double *lam; /* nLam */
464
+ cx *n0; /* nLam */
465
+ cx *ng; /* nLam, set_top */
466
+ cx *cosThetaG; /* nLam, set_top (polarization-independent) */
467
+ cx *eta0; /* 2·nLam, pol-major */
468
+ cx *etaS; /* 2·nLam */
469
+ mat2 *Mb; /* 2·nLam completed-stack product */
470
+ double *logScaleB; /* 2·nLam */
471
+ } growing_eval;
472
+
473
+ TMM_EXPORT("tmm_growing_eval_free")
474
+ void tmm_growing_eval_free(growing_eval *h) {
475
+ if (!h) return;
476
+ free(h->lam); free(h->n0); free(h->ng); free(h->cosThetaG);
477
+ free(h->eta0); free(h->etaS); free(h->Mb); free(h->logScaleB);
478
+ free(h);
479
+ }
480
+
481
+ TMM_EXPORT("tmm_growing_eval_create")
482
+ growing_eval *tmm_growing_eval_create(const double *lambdas, int nLam,
483
+ double theta_deg,
484
+ const double *n0arr, const double *nsarr,
485
+ const double *matNK, const double *thick,
486
+ int NB) {
487
+ growing_eval *h = (growing_eval *)malloc(sizeof(growing_eval));
488
+ if (!h) return 0;
489
+ h->nLam = nLam;
490
+ h->topSet = 0;
491
+ h->lam = (double *)malloc(sizeof(double) * nLam);
492
+ h->n0 = (cx *)malloc(sizeof(cx) * nLam);
493
+ h->ng = (cx *)malloc(sizeof(cx) * nLam);
494
+ h->cosThetaG = (cx *)malloc(sizeof(cx) * nLam);
495
+ h->eta0 = (cx *)malloc(sizeof(cx) * 2 * nLam);
496
+ h->etaS = (cx *)malloc(sizeof(cx) * 2 * nLam);
497
+ h->Mb = (mat2 *)malloc(sizeof(mat2) * 2 * nLam);
498
+ h->logScaleB = (double *)malloc(sizeof(double) * 2 * nLam);
499
+ if (!h->lam || !h->n0 || !h->ng || !h->cosThetaG
500
+ || !h->eta0 || !h->etaS || !h->Mb || !h->logScaleB) {
501
+ tmm_growing_eval_free(h);
502
+ return 0;
503
+ }
504
+
505
+ h->sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
506
+ cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(h->sinTheta0, h->sinTheta0)));
507
+
508
+ for (int li = 0; li < nLam; li++) {
509
+ h->lam[li] = lambdas[li];
510
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
511
+ cx ns = cmk(nsarr[2 * li + 0], nsarr[2 * li + 1]);
512
+ h->n0[li] = n0;
513
+ for (int pol = 0; pol < 2; pol++) {
514
+ size_t at = (size_t)pol * nLam + li;
515
+ h->eta0[at] = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
516
+ cx cosThetaS = snellCosTheta(n0, h->sinTheta0, ns);
517
+ h->etaS[at] = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
518
+ mat2 M;
519
+ M.a = cmk(1.0, 0.0); M.b = cmk(0.0, 0.0);
520
+ M.c = cmk(0.0, 0.0); M.d = cmk(1.0, 0.0);
521
+ double logScale = 0.0;
522
+ for (int k = 0; k < NB; k++) {
523
+ double d = thick[k];
524
+ if (d <= 0.0) continue;
525
+ cx n = cmk(matNK[((size_t)k * nLam + li) * 2 + 0],
526
+ matNK[((size_t)k * nLam + li) * 2 + 1]);
527
+ cx cosThetaJ = snellCosTheta(n0, h->sinTheta0, n);
528
+ M = matmul(M, layerMatrix(n, d, lambdas[li], cosThetaJ, pol));
529
+ logScale += rescaleMatrix(&M);
530
+ }
531
+ h->Mb[at] = M;
532
+ h->logScaleB[at] = logScale;
533
+ }
534
+ }
535
+ return h;
536
+ }
537
+
538
+ TMM_EXPORT("tmm_growing_eval_set_top")
539
+ void tmm_growing_eval_set_top(growing_eval *h, const double *ngNK) {
540
+ if (!h) return;
541
+ for (int li = 0; li < h->nLam; li++) {
542
+ cx ng = cmk(ngNK[2 * li + 0], ngNK[2 * li + 1]);
543
+ h->ng[li] = ng;
544
+ h->cosThetaG[li] = snellCosTheta(h->n0[li], h->sinTheta0, ng);
545
+ }
546
+ h->topSet = 1;
547
+ }
548
+
549
+ /* Returns 1 on success; 0, with the outputs untouched, for a null handle or
550
+ * a d > 0 sample before set_top declared the growing layer. The status is
551
+ * what keeps an unwritten buffer from being read back as data. */
552
+ TMM_EXPORT("tmm_growing_eval_sample")
553
+ int tmm_growing_eval_sample(growing_eval *h, double d,
554
+ double *outRs, double *outTs,
555
+ double *outRp, double *outTp,
556
+ double *outRrs, double *outRrp) {
557
+ if (!h || (d > 0.0 && !h->topSet)) return 0;
558
+ for (int li = 0; li < h->nLam; li++) {
559
+ for (int pol = 0; pol < 2; pol++) {
560
+ size_t at = (size_t)pol * h->nLam + li;
561
+ mat2 M = h->Mb[at];
562
+ double logScale = h->logScaleB[at];
563
+ if (d > 0.0) {
564
+ M = matmul(layerMatrix(h->ng[li], d, h->lam[li], h->cosThetaG[li], pol),
565
+ h->Mb[at]);
566
+ logScale += rescaleMatrix(&M);
567
+ }
568
+ double *oR = pol ? outRp : outRs;
569
+ double *oT = pol ? outTp : outTs;
570
+ double *oRr = pol ? outRrp : outRrs;
571
+ growingTailFwd(M, h->eta0[at], h->etaS[at], logScale, &oR[li], &oT[li]);
572
+ oRr[li] = growingTailRev(M, h->eta0[at], h->etaS[at]);
573
+ }
574
+ }
575
+ return 1;
576
+ }
577
+
243
578
  /* ── Exported: analytic thickness Jacobian for one (λ, θ, pol) ────────────────
244
579
  * 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
580
+ * analytic dR/dd_k, dT/dd_k, dA/dd_k for every layer at one sample : the DLS
246
581
  * refiner's per-step gradient (2·N fewer evals than central differences).
247
582
  *
248
583
  * `layers` is N triples [n_re, n_im, d]; layers used AS-IS (no d>0 filter) for
@@ -317,7 +652,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
317
652
  vec2 dV = cmatvec(Pre[k], cmatvec(dMk, Post[k + 1]));
318
653
  cx dB = dV.x, dC = dV.y;
319
654
 
320
- /* metrics(dB,dC) verbatim from validated tmmNeedleScan.metrics */
655
+ /* metrics(dB,dC) : verbatim from validated tmmNeedleScan.metrics */
321
656
  cx dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
322
657
  double dR = 2.0 * (cmul(cconj(r), dr)).re;
323
658
  cx dt = cmul(neg1, cmul(f, cadd(cmul(eta0, dB), dC)));
@@ -329,7 +664,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
329
664
  }
330
665
 
331
666
  /* ── Analytic needle P-function scan ─────────────────────────────────────────
332
- * Faithful port of tmmNeedleScan() in thinFilmMath.js the d→0 limit of
667
+ * Faithful port of tmmNeedleScan() in thinFilmMath.js : the d→0 limit of
333
668
  * Sullivan's pre/post method (Tikhonravov's analytic P-function). Returns the
334
669
  * merit-gradient ingredients {dR,dT,dA} of inserting an infinitesimal needle of
335
670
  * each candidate index at every gap position (0..N) and, optionally, at intra-
@@ -342,7 +677,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
342
677
  * intra : N*nFrac*nCand*3 layout [layer][frac][cand][dR,dT,dA] (nFrac>0)
343
678
  */
344
679
 
345
- /* {dR,dT,dA} from d[B,C]/dd verbatim from tmm_jacobian.metrics. */
680
+ /* {dR,dT,dA} from d[B,C]/dd : verbatim from tmm_jacobian.metrics. */
346
681
  static void needle_metrics(cx Bv, cx Cv, cx eta0, cx f, cx r, cx t, double Tfac,
347
682
  cx dB, cx dC, double *o) {
348
683
  cx dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
@@ -450,7 +785,7 @@ void tmm_needle_scan(double lambda_nm, double theta_deg, int pol,
450
785
  }
451
786
 
452
787
  /* ── Analytic thickness-Hessian kernel ───────────
453
- * LINE-BY-LINE port of tmmThicknessHessian() in thinFilmMath.js the EXACT
788
+ * LINE-BY-LINE port of tmmThicknessHessian() in thinFilmMath.js : the EXACT
454
789
  * analytic second derivatives ∂²{R,T,A}/∂dᵢ∂dⱼ (full N×N symmetric) plus the
455
790
  * first derivatives, at one (λ,θ,pol). Used by the bounded-SQP / Newton inner
456
791
  * refiner; the JS remains the oracle (tests/wasm_hessian_equivalence.mjs).
@@ -587,3 +922,459 @@ void tmm_hessian(double lambda_nm, double theta_deg, int pol,
587
922
  free(cosThJ); free(Ms); free(Pre); free(Post);
588
923
  free(dM); free(d2M); free(v); free(dBa); free(dCa);
589
924
  }
925
+
926
+ /* ── Third-order Taylor jets ─────────────────────────────────────────────────
927
+ * A jet holds [f, f', f''/2!, f'''/3!], each entry complex. Ordinary power-
928
+ * series algebra on these differentiates a function exactly, with no finite
929
+ * differences. Port of taylorJet.js; where the JS multiplies by a reciprocal
930
+ * rather than dividing, so does this, since the two are not bit-identical. */
931
+
932
+ #define JET_N 4
933
+
934
+ typedef struct { cx c[JET_N]; } jet;
935
+
936
+ static inline jet jconst(double re, double im) {
937
+ jet j;
938
+ j.c[0] = cmk(re, im);
939
+ j.c[1] = cmk(0.0, 0.0); j.c[2] = cmk(0.0, 0.0); j.c[3] = cmk(0.0, 0.0);
940
+ return j;
941
+ }
942
+ static inline jet jread(const double *p) {
943
+ jet j;
944
+ for (int i = 0; i < JET_N; i++) j.c[i] = cmk(p[2 * i], p[2 * i + 1]);
945
+ return j;
946
+ }
947
+ static inline jet jadd(jet a, jet b) {
948
+ jet o; for (int i = 0; i < JET_N; i++) o.c[i] = cadd(a.c[i], b.c[i]); return o;
949
+ }
950
+ static inline jet jsub(jet a, jet b) {
951
+ jet o; for (int i = 0; i < JET_N; i++) o.c[i] = csub(a.c[i], b.c[i]); return o;
952
+ }
953
+ static inline jet jscale(jet a, double s) {
954
+ jet o; for (int i = 0; i < JET_N; i++) o.c[i] = cmk(a.c[i].re * s, a.c[i].im * s); return o;
955
+ }
956
+ static inline jet jmul(jet a, jet b) {
957
+ jet o;
958
+ for (int order = 0; order < JET_N; order++) {
959
+ cx sum = cmk(0.0, 0.0);
960
+ for (int i = 0; i <= order; i++) sum = cadd(sum, cmul(a.c[i], b.c[order - i]));
961
+ o.c[order] = sum;
962
+ }
963
+ return o;
964
+ }
965
+ static inline jet jrecip(jet a) {
966
+ jet o;
967
+ o.c[0] = cdiv(cmk(1.0, 0.0), a.c[0]);
968
+ for (int order = 1; order < JET_N; order++) {
969
+ cx sum = cmk(0.0, 0.0);
970
+ for (int i = 1; i <= order; i++) sum = cadd(sum, cmul(a.c[i], o.c[order - i]));
971
+ cx q = cdiv(sum, a.c[0]);
972
+ o.c[order] = cmk(-q.re, -q.im);
973
+ }
974
+ return o;
975
+ }
976
+ static inline jet jdiv(jet a, jet b) { return jmul(a, jrecip(b)); }
977
+
978
+ static inline jet jsqrt_j(jet a) {
979
+ jet o;
980
+ o.c[0] = csqrt_(a.c[0]);
981
+ cx twiceRoot = cmk(o.c[0].re * 2.0, o.c[0].im * 2.0);
982
+ for (int order = 1; order < JET_N; order++) {
983
+ cx known = cmk(0.0, 0.0);
984
+ for (int i = 1; i < order; i++) known = cadd(known, cmul(o.c[i], o.c[order - i]));
985
+ o.c[order] = cdiv(csub(a.c[order], known), twiceRoot);
986
+ }
987
+ return o;
988
+ }
989
+
990
+ static void jsincos(jet a, jet *sine, jet *cosine) {
991
+ double re = a.c[0].re, im = a.c[0].im;
992
+ sine->c[0] = cmk(sin(re) * cosh(im), cos(re) * sinh(im));
993
+ cosine->c[0] = cmk(cos(re) * cosh(im), -sin(re) * sinh(im));
994
+ for (int order = 1; order < JET_N; order++) {
995
+ cx sineSum = cmk(0.0, 0.0), cosineSum = cmk(0.0, 0.0);
996
+ for (int i = 1; i <= order; i++) {
997
+ cx ts = cmul(a.c[i], cosine->c[order - i]);
998
+ cx tc = cmul(a.c[i], sine->c[order - i]);
999
+ sineSum = cadd(sineSum, cmk(ts.re * i, ts.im * i));
1000
+ cosineSum = cadd(cosineSum, cmk(tc.re * i, tc.im * i));
1001
+ }
1002
+ double inv = 1.0 / (double)order;
1003
+ sine->c[order] = cmk( sineSum.re * inv, sineSum.im * inv);
1004
+ cosine->c[order] = cmk(-cosineSum.re * inv, -cosineSum.im * inv);
1005
+ }
1006
+ }
1007
+
1008
+ /* Past the limit the layer is opaque: the derivatives are zero to machine
1009
+ * precision and dropping them keeps cosh from overflowing the whole product. */
1010
+ static inline jet jclampim(jet a, double limit) {
1011
+ if (a.c[0].im > limit || a.c[0].im < -limit) {
1012
+ double held = (a.c[0].im > limit) ? limit : -limit;
1013
+ jet o;
1014
+ o.c[0] = cmk(a.c[0].re, held);
1015
+ for (int i = 1; i < JET_N; i++) o.c[i] = cmk(a.c[i].re, 0.0);
1016
+ return o;
1017
+ }
1018
+ return a;
1019
+ }
1020
+
1021
+ /* [f, f', f'', f'''] from the stored [f, f', f''/2!, f'''/3!]. */
1022
+ static inline void jderivs(jet a, cx *out) {
1023
+ out[0] = a.c[0];
1024
+ out[1] = a.c[1];
1025
+ out[2] = cmk(a.c[2].re * 2.0, a.c[2].im * 2.0);
1026
+ out[3] = cmk(a.c[3].re * 6.0, a.c[3].im * 6.0);
1027
+ }
1028
+
1029
+ /* λ(ω) = 2πc/ω. Needs no value for c: with λ and ω given, λ' = −λ/ω. */
1030
+ static inline jet jwavelength(double lambda, double omega) {
1031
+ jet o;
1032
+ o.c[0] = cmk(lambda, 0.0);
1033
+ o.c[1] = cmk(-lambda / omega, 0.0);
1034
+ o.c[2] = cmk(lambda / (omega * omega), 0.0);
1035
+ o.c[3] = cmk(-lambda / (omega * omega * omega), 0.0);
1036
+ return o;
1037
+ }
1038
+
1039
+ /* ── Jet-valued 2×2 matrices ─────────────────────────────────────────────── */
1040
+
1041
+ typedef struct { jet a, b, c, d; } jmat2;
1042
+
1043
+ static jmat2 jmatmul(jmat2 A, jmat2 B) {
1044
+ jmat2 M;
1045
+ M.a = jadd(jmul(A.a, B.a), jmul(A.b, B.c));
1046
+ M.b = jadd(jmul(A.a, B.b), jmul(A.b, B.d));
1047
+ M.c = jadd(jmul(A.c, B.a), jmul(A.d, B.c));
1048
+ M.d = jadd(jmul(A.c, B.b), jmul(A.d, B.d));
1049
+ return M;
1050
+ }
1051
+ static jmat2 jidentity(void) {
1052
+ jmat2 M;
1053
+ M.a = jconst(1.0, 0.0); M.b = jconst(0.0, 0.0);
1054
+ M.c = jconst(0.0, 0.0); M.d = jconst(1.0, 0.0);
1055
+ return M;
1056
+ }
1057
+ static jmat2 jzero(void) {
1058
+ jmat2 M;
1059
+ M.a = jconst(0.0, 0.0); M.b = jconst(0.0, 0.0);
1060
+ M.c = jconst(0.0, 0.0); M.d = jconst(0.0, 0.0);
1061
+ return M;
1062
+ }
1063
+ /* The order-0 matrix controls overflow in the physical coefficient. Once
1064
+ * selected, one plain scalar rescales every jet order and cancels from r. */
1065
+ static double jrescale(jmat2 *M, double threshold) {
1066
+ jet *e[4] = { &M->a, &M->b, &M->c, &M->d };
1067
+ double scale = 0.0;
1068
+ for (int i = 0; i < 4; i++) {
1069
+ scale = fmax(scale, fabs(e[i]->c[0].re));
1070
+ scale = fmax(scale, fabs(e[i]->c[0].im));
1071
+ }
1072
+ if (scale <= threshold) return 0.0;
1073
+ double inverse = 1.0 / scale;
1074
+ for (int i = 0; i < 4; i++) *e[i] = jscale(*e[i], inverse);
1075
+ return log(scale);
1076
+ }
1077
+ static double jmatmag(jmat2 M) {
1078
+ jet *e[4] = { &M.a, &M.b, &M.c, &M.d };
1079
+ double magnitude = 0.0;
1080
+ for (int i = 0; i < 4; i++)
1081
+ for (int o = 0; o < JET_N; o++)
1082
+ magnitude = fmax(magnitude, fmax(fabs(e[i]->c[o].re), fabs(e[i]->c[o].im)));
1083
+ return magnitude;
1084
+ }
1085
+
1086
+ static inline jet jsnell_cos(jet n0, jet sin0, jet nj) {
1087
+ jet s = jdiv(jmul(n0, sin0), nj);
1088
+ return jsqrt_j(jsub(jconst(1.0, 0.0), jmul(s, s)));
1089
+ }
1090
+ static inline jet jadmittance(jet n, jet cosv, int pol) {
1091
+ return (pol == 0) ? jmul(n, cosv) : jdiv(n, cosv);
1092
+ }
1093
+
1094
+ static jmat2 jlayer_matrix(jet index, double thickness, jet wavelength, jet cosine, int pol) {
1095
+ jet phase = jclampim(jscale(jdiv(jmul(index, cosine), wavelength),
1096
+ 2.0 * PI * thickness), MAX_IM_DELTA);
1097
+ jet sine, cosinePhase;
1098
+ jsincos(phase, &sine, &cosinePhase);
1099
+ jet eta = jadmittance(index, cosine, pol);
1100
+ jet minusI = jconst(0.0, -1.0);
1101
+ jmat2 M;
1102
+ M.a = cosinePhase;
1103
+ M.b = jmul(minusI, jdiv(sine, eta));
1104
+ M.c = jmul(minusI, jmul(eta, sine));
1105
+ M.d = cosinePhase;
1106
+ return M;
1107
+ }
1108
+
1109
+ static void jlayer_matrix_dd(jet index, double thickness, jet wavelength, jet cosine, int pol,
1110
+ jmat2 *M, jmat2 *dM) {
1111
+ jet phasePerUnit = jscale(jdiv(jmul(index, cosine), wavelength), 2.0 * PI);
1112
+ jet rawPhase = jscale(phasePerUnit, thickness);
1113
+ jet phase = jclampim(rawPhase, MAX_IM_DELTA);
1114
+ jet phaseDerivative;
1115
+ if (rawPhase.c[0].im == phase.c[0].im) {
1116
+ phaseDerivative = phasePerUnit;
1117
+ } else {
1118
+ for (int i = 0; i < JET_N; i++)
1119
+ phaseDerivative.c[i] = cmk(phasePerUnit.c[i].re, 0.0);
1120
+ }
1121
+ jet sine, cosinePhase;
1122
+ jsincos(phase, &sine, &cosinePhase);
1123
+ jet sineDerivative = jmul(cosinePhase, phaseDerivative);
1124
+ jet cosineDerivative = jscale(jmul(sine, phaseDerivative), -1.0);
1125
+ jet eta = jadmittance(index, cosine, pol);
1126
+ jet minusI = jconst(0.0, -1.0);
1127
+ M->a = cosinePhase;
1128
+ M->b = jmul(minusI, jdiv(sine, eta));
1129
+ M->c = jmul(minusI, jmul(eta, sine));
1130
+ M->d = cosinePhase;
1131
+ dM->a = cosineDerivative;
1132
+ dM->b = jmul(minusI, jdiv(sineDerivative, eta));
1133
+ dM->c = jmul(minusI, jmul(eta, sineDerivative));
1134
+ dM->d = cosineDerivative;
1135
+ }
1136
+
1137
+ typedef struct { jet reflection, transmission, denominator; } jcoef;
1138
+
1139
+ static jcoef jcoef_from_matrix(jmat2 M, jet incidentEta, jet substrateEta, double logScale) {
1140
+ jet boundaryB = jadd(M.a, jmul(M.b, substrateEta));
1141
+ jet boundaryC = jadd(M.c, jmul(M.d, substrateEta));
1142
+ jet incidentB = jmul(incidentEta, boundaryB);
1143
+ jcoef o;
1144
+ o.denominator = jadd(incidentB, boundaryC);
1145
+ o.reflection = jdiv(jsub(incidentB, boundaryC), o.denominator);
1146
+ o.transmission = jdiv(jscale(incidentEta, 2.0), o.denominator);
1147
+ if (logScale != 0.0) o.transmission = jscale(o.transmission, exp(-logScale));
1148
+ return o;
1149
+ }
1150
+
1151
+ static void jcoef_thickness(jmat2 dMat, jcoef base, jet incidentEta, jet substrateEta,
1152
+ jet *dReflection, jet *dTransmission) {
1153
+ jet dB = jadd(dMat.a, jmul(dMat.b, substrateEta));
1154
+ jet dC = jadd(dMat.c, jmul(dMat.d, substrateEta));
1155
+ jet dIncidentB = jmul(incidentEta, dB);
1156
+ jet dDenominator = jadd(dIncidentB, dC);
1157
+ jet dNumerator = jsub(dIncidentB, dC);
1158
+ *dReflection = jdiv(jsub(dNumerator, jmul(base.reflection, dDenominator)), base.denominator);
1159
+ *dTransmission = jscale(jdiv(jmul(base.transmission, dDenominator), base.denominator), -1.0);
1160
+ }
1161
+
1162
+ /* ── Phase quantities from a coefficient jet ─────────────────────────────────
1163
+ * Writes [phaseRad, GD, GDD, TOD, |coefficient|²]; all NaN where the
1164
+ * coefficient is exactly zero and the phase is undefined.
1165
+ *
1166
+ * GD = Im(r'/r)
1167
+ * GDD = Im(r''/r − (r'/r)²)
1168
+ * TOD = Im(r'''/r − 3 r'r''/r² + 2 (r'/r)³) Birge & Kärtner
1169
+ *
1170
+ * GD comes out in the reciprocal of the caller's ω unit, GDD in its square and
1171
+ * TOD in its cube. */
1172
+
1173
+ static void jphase(jet coefficient, double *out5) {
1174
+ cx d[4];
1175
+ jderivs(coefficient, d);
1176
+ cx value = d[0];
1177
+ double magnitudeSquared = value.re * value.re + value.im * value.im;
1178
+ if (magnitudeSquared == 0.0 || !isfinite(magnitudeSquared)) {
1179
+ for (int i = 0; i < 5; i++) out5[i] = NAN;
1180
+ return;
1181
+ }
1182
+ cx inverse = cdiv(cmk(1.0, 0.0), value);
1183
+ cx firstRatio = cmul(d[1], inverse);
1184
+ cx secondRatio = cmul(d[2], inverse);
1185
+ cx thirdRatio = cmul(d[3], inverse);
1186
+ cx squareFirst = cmk(firstRatio.re * firstRatio.re - firstRatio.im * firstRatio.im,
1187
+ 2.0 * firstRatio.re * firstRatio.im);
1188
+ cx firstTimesSecond = cmk(
1189
+ firstRatio.re * secondRatio.re - firstRatio.im * secondRatio.im,
1190
+ firstRatio.re * secondRatio.im + firstRatio.im * secondRatio.re);
1191
+ cx cubeFirst = cmk(squareFirst.re * firstRatio.re - squareFirst.im * firstRatio.im,
1192
+ squareFirst.re * firstRatio.im + squareFirst.im * firstRatio.re);
1193
+ out5[0] = -atan2(value.im, value.re);
1194
+ out5[1] = firstRatio.im;
1195
+ out5[2] = secondRatio.im - squareFirst.im;
1196
+ out5[3] = thirdRatio.im - 3.0 * firstTimesSecond.im + 2.0 * cubeFirst.im;
1197
+ out5[4] = magnitudeSquared;
1198
+ }
1199
+
1200
+ /* ── Phase core: one wavelength, both coefficients ───────────────────────────
1201
+ * `sinJet` is the incident-side sine as a jet, for a stack embedded in a
1202
+ * dispersive medium at a fixed external angle; NULL uses the constant
1203
+ * sin(theta_deg). out10 = [r: phaseRad, GD, GDD, TOD, |r|²][t: same]. */
1204
+
1205
+ static void jphase_core(double lambda, double omega, double theta_deg, int pol,
1206
+ jet n0, jet ns, const jet *layerN, const double *thick, int N,
1207
+ const jet *sinJet, double *out10) {
1208
+ jet wavelength = jwavelength(lambda, omega);
1209
+ jet incidentSine = sinJet ? *sinJet : jconst(sin(theta_deg * PI / 180.0), 0.0);
1210
+ jet incidentCosine = sinJet
1211
+ ? jsqrt_j(jsub(jconst(1.0, 0.0), jmul(incidentSine, incidentSine)))
1212
+ : jconst(cos(theta_deg * PI / 180.0), 0.0);
1213
+ jet incidentEta = jadmittance(n0, incidentCosine, pol);
1214
+ jet substrateCosine = jsnell_cos(n0, incidentSine, ns);
1215
+ jet substrateEta = jadmittance(ns, substrateCosine, pol);
1216
+
1217
+ jmat2 M = jidentity();
1218
+ double logScale = 0.0;
1219
+ for (int k = 0; k < N; k++) {
1220
+ if (!(thick[k] > 0.0)) continue;
1221
+ jet cosine = jsnell_cos(n0, incidentSine, layerN[k]);
1222
+ M = jmatmul(M, jlayer_matrix(layerN[k], thick[k], wavelength, cosine, pol));
1223
+ logScale += jrescale(&M, MATRIX_RESCALE_THRESHOLD);
1224
+ }
1225
+ jcoef coefficients = jcoef_from_matrix(M, incidentEta, substrateEta, logScale);
1226
+ jphase(coefficients.reflection, &out10[0]);
1227
+ jphase(coefficients.transmission, &out10[5]);
1228
+ }
1229
+
1230
+ /* ── Exported: phase dispersion at one wavelength ─────────────────────────────
1231
+ * Mirrors tmmPhaseDispersion() in phase.js. Index jets are 8 doubles each,
1232
+ * [re,im] per order. `sinJet` may be NULL. */
1233
+
1234
+ TMM_EXPORT("tmm_phase_one")
1235
+ void tmm_phase_one(double lambda, double omega, double theta_deg, int pol,
1236
+ const double *n0jet, const double *nsjet,
1237
+ const double *layerJets, const double *thick, int N,
1238
+ const double *sinJet, double *out) {
1239
+ jet *layerN = (jet *)malloc(sizeof(jet) * (N > 0 ? N : 1));
1240
+ for (int k = 0; k < N; k++) layerN[k] = jread(&layerJets[8 * k]);
1241
+ jet sine;
1242
+ if (sinJet) sine = jread(sinJet);
1243
+ jphase_core(lambda, omega, theta_deg, pol,
1244
+ jread(n0jet), jread(nsjet), layerN, thick, N,
1245
+ sinJet ? &sine : NULL, out);
1246
+ free(layerN);
1247
+ }
1248
+
1249
+ /* ── Exported: batched phase dispersion over a wavelength grid ────────────────
1250
+ * One call evaluates the whole grid, amortizing the JS↔WASM boundary the same
1251
+ * way tmm_spectrum does. Polarization is an argument rather than both-at-once,
1252
+ * because this kernel is an order of magnitude dearer per sample than the plain
1253
+ * spectrum and callers at normal incidence would pay twice for nothing.
1254
+ *
1255
+ * Memory layout (all f64, caller-owned):
1256
+ * lambdas : nLam
1257
+ * omegas : nLam angular frequency per λ; sets the unit
1258
+ * n0jets : nLam × 8 incident-medium index jet per λ
1259
+ * nsjets : nLam × 8 substrate index jet per λ
1260
+ * matJets : N × nLam × 8 per-layer index jet, layout [layer][λ]
1261
+ * thick : N
1262
+ * sinJets : nLam × 8, or NULL
1263
+ * out : nLam × 10 [r: phaseRad,GD,GDD,TOD,|r|²][t: same] */
1264
+
1265
+ TMM_EXPORT("tmm_phase_spectrum")
1266
+ void tmm_phase_spectrum(const double *lambdas, const double *omegas, int nLam,
1267
+ const double *n0jets, const double *nsjets,
1268
+ const double *matJets, const double *thick, int N,
1269
+ double theta_deg, int pol,
1270
+ const double *sinJets, double *out) {
1271
+ jet *layerN = (jet *)malloc(sizeof(jet) * (N > 0 ? N : 1));
1272
+ for (int li = 0; li < nLam; li++) {
1273
+ for (int k = 0; k < N; k++) {
1274
+ long base = ((long)k * nLam + li) * 8;
1275
+ layerN[k] = jread(&matJets[base]);
1276
+ }
1277
+ jet sine;
1278
+ if (sinJets) sine = jread(&sinJets[8 * (long)li]);
1279
+ jphase_core(lambdas[li], omegas[li], theta_deg, pol,
1280
+ jread(&n0jets[8 * (long)li]), jread(&nsjets[8 * (long)li]),
1281
+ layerN, thick, N,
1282
+ sinJets ? &sine : NULL, &out[10 * (long)li]);
1283
+ }
1284
+ free(layerN);
1285
+ }
1286
+
1287
+ /* ── Exported: phase dispersion plus exact thickness derivatives ──────────────
1288
+ * Mirrors tmmPhaseThicknessJacobian() in phase.js. Frequency stays the Taylor
1289
+ * variable, so each thickness derivative is itself a third-order frequency jet.
1290
+ * Zero-thickness layers are retained so derivative indices line up with the
1291
+ * caller's design array. Negative and NaN thicknesses are skipped with a zero
1292
+ * derivative, matching the point evaluator's base result.
1293
+ *
1294
+ * out : 10 as tmm_phase_one
1295
+ * deriv : 8 × N [side][quantity][layer], side 0 = r, 1 = t,
1296
+ * quantity 0 = dPhaseDeg, 1 = dGD, 2 = dGDD, 3 = dTOD
1297
+ *
1298
+ * The prefix/suffix decomposition cannot carry a rescaling, so if the matrix
1299
+ * product overflows, `out` is still filled from the plain path and every entry
1300
+ * of `deriv` is set to NaN. */
1301
+
1302
+ TMM_EXPORT("tmm_phase_jacobian")
1303
+ void tmm_phase_jacobian(double lambda, double omega, double theta_deg, int pol,
1304
+ const double *n0jet, const double *nsjet,
1305
+ const double *layerJets, const double *thick, int N,
1306
+ const double *sinJet, double *out, double *deriv) {
1307
+ jet n0 = jread(n0jet), ns = jread(nsjet);
1308
+ jet *layerN = (jet *)malloc(sizeof(jet) * (N > 0 ? N : 1));
1309
+ for (int k = 0; k < N; k++) layerN[k] = jread(&layerJets[8 * k]);
1310
+ jet sine;
1311
+ if (sinJet) sine = jread(sinJet);
1312
+ const jet *sinePtr = sinJet ? &sine : NULL;
1313
+
1314
+ jet wavelength = jwavelength(lambda, omega);
1315
+ jet incidentSine = sinePtr ? *sinePtr : jconst(sin(theta_deg * PI / 180.0), 0.0);
1316
+ jet incidentCosine = sinePtr
1317
+ ? jsqrt_j(jsub(jconst(1.0, 0.0), jmul(incidentSine, incidentSine)))
1318
+ : jconst(cos(theta_deg * PI / 180.0), 0.0);
1319
+ jet incidentEta = jadmittance(n0, incidentCosine, pol);
1320
+ jet substrateCosine = jsnell_cos(n0, incidentSine, ns);
1321
+ jet substrateEta = jadmittance(ns, substrateCosine, pol);
1322
+
1323
+ int M = (N > 0 ? N : 1);
1324
+ jmat2 *layerM = (jmat2 *)malloc(sizeof(jmat2) * M);
1325
+ jmat2 *layerDM = (jmat2 *)malloc(sizeof(jmat2) * M);
1326
+ for (int k = 0; k < N; k++) {
1327
+ /* Match jphase_core's skip rule for invalid negative/NaN thicknesses,
1328
+ * while retaining the useful derivative of a zero-thickness layer. */
1329
+ if (!(thick[k] >= 0.0)) {
1330
+ layerM[k] = jidentity();
1331
+ layerDM[k] = jzero();
1332
+ continue;
1333
+ }
1334
+ jet cosine = jsnell_cos(n0, incidentSine, layerN[k]);
1335
+ jlayer_matrix_dd(layerN[k], thick[k], wavelength, cosine, pol,
1336
+ &layerM[k], &layerDM[k]);
1337
+ }
1338
+
1339
+ jmat2 *prefix = (jmat2 *)malloc(sizeof(jmat2) * (N + 1));
1340
+ jmat2 *suffix = (jmat2 *)malloc(sizeof(jmat2) * (N + 1));
1341
+ prefix[0] = jidentity();
1342
+ int overflowed = 0;
1343
+ for (int k = 0; k < N; k++) {
1344
+ prefix[k + 1] = jmatmul(prefix[k], layerM[k]);
1345
+ if (jmatmag(prefix[k + 1]) > MATRIX_RESCALE_THRESHOLD) { overflowed = 1; break; }
1346
+ }
1347
+
1348
+ if (overflowed) {
1349
+ jphase_core(lambda, omega, theta_deg, pol, n0, ns, layerN, thick, N, sinePtr, out);
1350
+ for (long i = 0; i < 8L * N; i++) deriv[i] = NAN;
1351
+ free(layerN); free(layerM); free(layerDM); free(prefix); free(suffix);
1352
+ return;
1353
+ }
1354
+
1355
+ suffix[N] = jidentity();
1356
+ for (int k = N - 1; k >= 0; k--) suffix[k] = jmatmul(layerM[k], suffix[k + 1]);
1357
+
1358
+ jcoef coefficients = jcoef_from_matrix(prefix[N], incidentEta, substrateEta, 0.0);
1359
+ jphase(coefficients.reflection, &out[0]);
1360
+ jphase(coefficients.transmission, &out[5]);
1361
+
1362
+ for (int k = 0; k < N; k++) {
1363
+ jmat2 matrixDerivative = jmatmul(jmatmul(prefix[k], layerDM[k]), suffix[k + 1]);
1364
+ jet dReflection, dTransmission;
1365
+ jcoef_thickness(matrixDerivative, coefficients, incidentEta, substrateEta,
1366
+ &dReflection, &dTransmission);
1367
+ const jet sides[2] = { dReflection, dTransmission };
1368
+ const jet base[2] = { coefficients.reflection, coefficients.transmission };
1369
+ for (int s = 0; s < 2; s++) {
1370
+ cx dd[4];
1371
+ jderivs(jdiv(sides[s], base[s]), dd);
1372
+ deriv[((long)s * 4 + 0) * N + k] = -dd[0].im * 180.0 / PI;
1373
+ deriv[((long)s * 4 + 1) * N + k] = dd[1].im;
1374
+ deriv[((long)s * 4 + 2) * N + k] = dd[2].im;
1375
+ deriv[((long)s * 4 + 3) * N + k] = dd[3].im;
1376
+ }
1377
+ }
1378
+
1379
+ free(layerN); free(layerM); free(layerDM); free(prefix); free(suffix);
1380
+ }