tmmcore 0.1.0 → 0.2.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));
@@ -242,7 +247,7 @@ void tmm_spectrum(const double *lambdas, int nLam,
242
247
 
243
248
  /* ── Exported: analytic thickness Jacobian for one (λ, θ, pol) ────────────────
244
249
  * 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
250
+ * analytic dR/dd_k, dT/dd_k, dA/dd_k for every layer at one sample : the DLS
246
251
  * refiner's per-step gradient (2·N fewer evals than central differences).
247
252
  *
248
253
  * `layers` is N triples [n_re, n_im, d]; layers used AS-IS (no d>0 filter) for
@@ -317,7 +322,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
317
322
  vec2 dV = cmatvec(Pre[k], cmatvec(dMk, Post[k + 1]));
318
323
  cx dB = dV.x, dC = dV.y;
319
324
 
320
- /* metrics(dB,dC) verbatim from validated tmmNeedleScan.metrics */
325
+ /* metrics(dB,dC) : verbatim from validated tmmNeedleScan.metrics */
321
326
  cx dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
322
327
  double dR = 2.0 * (cmul(cconj(r), dr)).re;
323
328
  cx dt = cmul(neg1, cmul(f, cadd(cmul(eta0, dB), dC)));
@@ -329,7 +334,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
329
334
  }
330
335
 
331
336
  /* ── Analytic needle P-function scan ─────────────────────────────────────────
332
- * Faithful port of tmmNeedleScan() in thinFilmMath.js the d→0 limit of
337
+ * Faithful port of tmmNeedleScan() in thinFilmMath.js : the d→0 limit of
333
338
  * Sullivan's pre/post method (Tikhonravov's analytic P-function). Returns the
334
339
  * merit-gradient ingredients {dR,dT,dA} of inserting an infinitesimal needle of
335
340
  * each candidate index at every gap position (0..N) and, optionally, at intra-
@@ -342,7 +347,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
342
347
  * intra : N*nFrac*nCand*3 layout [layer][frac][cand][dR,dT,dA] (nFrac>0)
343
348
  */
344
349
 
345
- /* {dR,dT,dA} from d[B,C]/dd verbatim from tmm_jacobian.metrics. */
350
+ /* {dR,dT,dA} from d[B,C]/dd : verbatim from tmm_jacobian.metrics. */
346
351
  static void needle_metrics(cx Bv, cx Cv, cx eta0, cx f, cx r, cx t, double Tfac,
347
352
  cx dB, cx dC, double *o) {
348
353
  cx dr = cmul(f, csub(cmul(Cv, dB), cmul(Bv, dC)));
@@ -450,7 +455,7 @@ void tmm_needle_scan(double lambda_nm, double theta_deg, int pol,
450
455
  }
451
456
 
452
457
  /* ── Analytic thickness-Hessian kernel ───────────
453
- * LINE-BY-LINE port of tmmThicknessHessian() in thinFilmMath.js the EXACT
458
+ * LINE-BY-LINE port of tmmThicknessHessian() in thinFilmMath.js : the EXACT
454
459
  * analytic second derivatives ∂²{R,T,A}/∂dᵢ∂dⱼ (full N×N symmetric) plus the
455
460
  * first derivatives, at one (λ,θ,pol). Used by the bounded-SQP / Newton inner
456
461
  * refiner; the JS remains the oracle (tests/wasm_hessian_equivalence.mjs).
@@ -587,3 +592,459 @@ void tmm_hessian(double lambda_nm, double theta_deg, int pol,
587
592
  free(cosThJ); free(Ms); free(Pre); free(Post);
588
593
  free(dM); free(d2M); free(v); free(dBa); free(dCa);
589
594
  }
595
+
596
+ /* ── Third-order Taylor jets ─────────────────────────────────────────────────
597
+ * A jet holds [f, f', f''/2!, f'''/3!], each entry complex. Ordinary power-
598
+ * series algebra on these differentiates a function exactly, with no finite
599
+ * differences. Port of taylorJet.js; where the JS multiplies by a reciprocal
600
+ * rather than dividing, so does this, since the two are not bit-identical. */
601
+
602
+ #define JET_N 4
603
+
604
+ typedef struct { cx c[JET_N]; } jet;
605
+
606
+ static inline jet jconst(double re, double im) {
607
+ jet j;
608
+ j.c[0] = cmk(re, im);
609
+ j.c[1] = cmk(0.0, 0.0); j.c[2] = cmk(0.0, 0.0); j.c[3] = cmk(0.0, 0.0);
610
+ return j;
611
+ }
612
+ static inline jet jread(const double *p) {
613
+ jet j;
614
+ for (int i = 0; i < JET_N; i++) j.c[i] = cmk(p[2 * i], p[2 * i + 1]);
615
+ return j;
616
+ }
617
+ static inline jet jadd(jet a, jet b) {
618
+ jet o; for (int i = 0; i < JET_N; i++) o.c[i] = cadd(a.c[i], b.c[i]); return o;
619
+ }
620
+ static inline jet jsub(jet a, jet b) {
621
+ jet o; for (int i = 0; i < JET_N; i++) o.c[i] = csub(a.c[i], b.c[i]); return o;
622
+ }
623
+ static inline jet jscale(jet a, double s) {
624
+ 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;
625
+ }
626
+ static inline jet jmul(jet a, jet b) {
627
+ jet o;
628
+ for (int order = 0; order < JET_N; order++) {
629
+ cx sum = cmk(0.0, 0.0);
630
+ for (int i = 0; i <= order; i++) sum = cadd(sum, cmul(a.c[i], b.c[order - i]));
631
+ o.c[order] = sum;
632
+ }
633
+ return o;
634
+ }
635
+ static inline jet jrecip(jet a) {
636
+ jet o;
637
+ o.c[0] = cdiv(cmk(1.0, 0.0), a.c[0]);
638
+ for (int order = 1; order < JET_N; order++) {
639
+ cx sum = cmk(0.0, 0.0);
640
+ for (int i = 1; i <= order; i++) sum = cadd(sum, cmul(a.c[i], o.c[order - i]));
641
+ cx q = cdiv(sum, a.c[0]);
642
+ o.c[order] = cmk(-q.re, -q.im);
643
+ }
644
+ return o;
645
+ }
646
+ static inline jet jdiv(jet a, jet b) { return jmul(a, jrecip(b)); }
647
+
648
+ static inline jet jsqrt_j(jet a) {
649
+ jet o;
650
+ o.c[0] = csqrt_(a.c[0]);
651
+ cx twiceRoot = cmk(o.c[0].re * 2.0, o.c[0].im * 2.0);
652
+ for (int order = 1; order < JET_N; order++) {
653
+ cx known = cmk(0.0, 0.0);
654
+ for (int i = 1; i < order; i++) known = cadd(known, cmul(o.c[i], o.c[order - i]));
655
+ o.c[order] = cdiv(csub(a.c[order], known), twiceRoot);
656
+ }
657
+ return o;
658
+ }
659
+
660
+ static void jsincos(jet a, jet *sine, jet *cosine) {
661
+ double re = a.c[0].re, im = a.c[0].im;
662
+ sine->c[0] = cmk(sin(re) * cosh(im), cos(re) * sinh(im));
663
+ cosine->c[0] = cmk(cos(re) * cosh(im), -sin(re) * sinh(im));
664
+ for (int order = 1; order < JET_N; order++) {
665
+ cx sineSum = cmk(0.0, 0.0), cosineSum = cmk(0.0, 0.0);
666
+ for (int i = 1; i <= order; i++) {
667
+ cx ts = cmul(a.c[i], cosine->c[order - i]);
668
+ cx tc = cmul(a.c[i], sine->c[order - i]);
669
+ sineSum = cadd(sineSum, cmk(ts.re * i, ts.im * i));
670
+ cosineSum = cadd(cosineSum, cmk(tc.re * i, tc.im * i));
671
+ }
672
+ double inv = 1.0 / (double)order;
673
+ sine->c[order] = cmk( sineSum.re * inv, sineSum.im * inv);
674
+ cosine->c[order] = cmk(-cosineSum.re * inv, -cosineSum.im * inv);
675
+ }
676
+ }
677
+
678
+ /* Past the limit the layer is opaque: the derivatives are zero to machine
679
+ * precision and dropping them keeps cosh from overflowing the whole product. */
680
+ static inline jet jclampim(jet a, double limit) {
681
+ if (a.c[0].im > limit || a.c[0].im < -limit) {
682
+ double held = (a.c[0].im > limit) ? limit : -limit;
683
+ jet o;
684
+ o.c[0] = cmk(a.c[0].re, held);
685
+ for (int i = 1; i < JET_N; i++) o.c[i] = cmk(a.c[i].re, 0.0);
686
+ return o;
687
+ }
688
+ return a;
689
+ }
690
+
691
+ /* [f, f', f'', f'''] from the stored [f, f', f''/2!, f'''/3!]. */
692
+ static inline void jderivs(jet a, cx *out) {
693
+ out[0] = a.c[0];
694
+ out[1] = a.c[1];
695
+ out[2] = cmk(a.c[2].re * 2.0, a.c[2].im * 2.0);
696
+ out[3] = cmk(a.c[3].re * 6.0, a.c[3].im * 6.0);
697
+ }
698
+
699
+ /* λ(ω) = 2πc/ω. Needs no value for c: with λ and ω given, λ' = −λ/ω. */
700
+ static inline jet jwavelength(double lambda, double omega) {
701
+ jet o;
702
+ o.c[0] = cmk(lambda, 0.0);
703
+ o.c[1] = cmk(-lambda / omega, 0.0);
704
+ o.c[2] = cmk(lambda / (omega * omega), 0.0);
705
+ o.c[3] = cmk(-lambda / (omega * omega * omega), 0.0);
706
+ return o;
707
+ }
708
+
709
+ /* ── Jet-valued 2×2 matrices ─────────────────────────────────────────────── */
710
+
711
+ typedef struct { jet a, b, c, d; } jmat2;
712
+
713
+ static jmat2 jmatmul(jmat2 A, jmat2 B) {
714
+ jmat2 M;
715
+ M.a = jadd(jmul(A.a, B.a), jmul(A.b, B.c));
716
+ M.b = jadd(jmul(A.a, B.b), jmul(A.b, B.d));
717
+ M.c = jadd(jmul(A.c, B.a), jmul(A.d, B.c));
718
+ M.d = jadd(jmul(A.c, B.b), jmul(A.d, B.d));
719
+ return M;
720
+ }
721
+ static jmat2 jidentity(void) {
722
+ jmat2 M;
723
+ M.a = jconst(1.0, 0.0); M.b = jconst(0.0, 0.0);
724
+ M.c = jconst(0.0, 0.0); M.d = jconst(1.0, 0.0);
725
+ return M;
726
+ }
727
+ static jmat2 jzero(void) {
728
+ jmat2 M;
729
+ M.a = jconst(0.0, 0.0); M.b = jconst(0.0, 0.0);
730
+ M.c = jconst(0.0, 0.0); M.d = jconst(0.0, 0.0);
731
+ return M;
732
+ }
733
+ /* The order-0 matrix controls overflow in the physical coefficient. Once
734
+ * selected, one plain scalar rescales every jet order and cancels from r. */
735
+ static double jrescale(jmat2 *M, double threshold) {
736
+ jet *e[4] = { &M->a, &M->b, &M->c, &M->d };
737
+ double scale = 0.0;
738
+ for (int i = 0; i < 4; i++) {
739
+ scale = fmax(scale, fabs(e[i]->c[0].re));
740
+ scale = fmax(scale, fabs(e[i]->c[0].im));
741
+ }
742
+ if (scale <= threshold) return 0.0;
743
+ double inverse = 1.0 / scale;
744
+ for (int i = 0; i < 4; i++) *e[i] = jscale(*e[i], inverse);
745
+ return log(scale);
746
+ }
747
+ static double jmatmag(jmat2 M) {
748
+ jet *e[4] = { &M.a, &M.b, &M.c, &M.d };
749
+ double magnitude = 0.0;
750
+ for (int i = 0; i < 4; i++)
751
+ for (int o = 0; o < JET_N; o++)
752
+ magnitude = fmax(magnitude, fmax(fabs(e[i]->c[o].re), fabs(e[i]->c[o].im)));
753
+ return magnitude;
754
+ }
755
+
756
+ static inline jet jsnell_cos(jet n0, jet sin0, jet nj) {
757
+ jet s = jdiv(jmul(n0, sin0), nj);
758
+ return jsqrt_j(jsub(jconst(1.0, 0.0), jmul(s, s)));
759
+ }
760
+ static inline jet jadmittance(jet n, jet cosv, int pol) {
761
+ return (pol == 0) ? jmul(n, cosv) : jdiv(n, cosv);
762
+ }
763
+
764
+ static jmat2 jlayer_matrix(jet index, double thickness, jet wavelength, jet cosine, int pol) {
765
+ jet phase = jclampim(jscale(jdiv(jmul(index, cosine), wavelength),
766
+ 2.0 * PI * thickness), MAX_IM_DELTA);
767
+ jet sine, cosinePhase;
768
+ jsincos(phase, &sine, &cosinePhase);
769
+ jet eta = jadmittance(index, cosine, pol);
770
+ jet minusI = jconst(0.0, -1.0);
771
+ jmat2 M;
772
+ M.a = cosinePhase;
773
+ M.b = jmul(minusI, jdiv(sine, eta));
774
+ M.c = jmul(minusI, jmul(eta, sine));
775
+ M.d = cosinePhase;
776
+ return M;
777
+ }
778
+
779
+ static void jlayer_matrix_dd(jet index, double thickness, jet wavelength, jet cosine, int pol,
780
+ jmat2 *M, jmat2 *dM) {
781
+ jet phasePerUnit = jscale(jdiv(jmul(index, cosine), wavelength), 2.0 * PI);
782
+ jet rawPhase = jscale(phasePerUnit, thickness);
783
+ jet phase = jclampim(rawPhase, MAX_IM_DELTA);
784
+ jet phaseDerivative;
785
+ if (rawPhase.c[0].im == phase.c[0].im) {
786
+ phaseDerivative = phasePerUnit;
787
+ } else {
788
+ for (int i = 0; i < JET_N; i++)
789
+ phaseDerivative.c[i] = cmk(phasePerUnit.c[i].re, 0.0);
790
+ }
791
+ jet sine, cosinePhase;
792
+ jsincos(phase, &sine, &cosinePhase);
793
+ jet sineDerivative = jmul(cosinePhase, phaseDerivative);
794
+ jet cosineDerivative = jscale(jmul(sine, phaseDerivative), -1.0);
795
+ jet eta = jadmittance(index, cosine, pol);
796
+ jet minusI = jconst(0.0, -1.0);
797
+ M->a = cosinePhase;
798
+ M->b = jmul(minusI, jdiv(sine, eta));
799
+ M->c = jmul(minusI, jmul(eta, sine));
800
+ M->d = cosinePhase;
801
+ dM->a = cosineDerivative;
802
+ dM->b = jmul(minusI, jdiv(sineDerivative, eta));
803
+ dM->c = jmul(minusI, jmul(eta, sineDerivative));
804
+ dM->d = cosineDerivative;
805
+ }
806
+
807
+ typedef struct { jet reflection, transmission, denominator; } jcoef;
808
+
809
+ static jcoef jcoef_from_matrix(jmat2 M, jet incidentEta, jet substrateEta, double logScale) {
810
+ jet boundaryB = jadd(M.a, jmul(M.b, substrateEta));
811
+ jet boundaryC = jadd(M.c, jmul(M.d, substrateEta));
812
+ jet incidentB = jmul(incidentEta, boundaryB);
813
+ jcoef o;
814
+ o.denominator = jadd(incidentB, boundaryC);
815
+ o.reflection = jdiv(jsub(incidentB, boundaryC), o.denominator);
816
+ o.transmission = jdiv(jscale(incidentEta, 2.0), o.denominator);
817
+ if (logScale != 0.0) o.transmission = jscale(o.transmission, exp(-logScale));
818
+ return o;
819
+ }
820
+
821
+ static void jcoef_thickness(jmat2 dMat, jcoef base, jet incidentEta, jet substrateEta,
822
+ jet *dReflection, jet *dTransmission) {
823
+ jet dB = jadd(dMat.a, jmul(dMat.b, substrateEta));
824
+ jet dC = jadd(dMat.c, jmul(dMat.d, substrateEta));
825
+ jet dIncidentB = jmul(incidentEta, dB);
826
+ jet dDenominator = jadd(dIncidentB, dC);
827
+ jet dNumerator = jsub(dIncidentB, dC);
828
+ *dReflection = jdiv(jsub(dNumerator, jmul(base.reflection, dDenominator)), base.denominator);
829
+ *dTransmission = jscale(jdiv(jmul(base.transmission, dDenominator), base.denominator), -1.0);
830
+ }
831
+
832
+ /* ── Phase quantities from a coefficient jet ─────────────────────────────────
833
+ * Writes [phaseRad, GD, GDD, TOD, |coefficient|²]; all NaN where the
834
+ * coefficient is exactly zero and the phase is undefined.
835
+ *
836
+ * GD = Im(r'/r)
837
+ * GDD = Im(r''/r − (r'/r)²)
838
+ * TOD = Im(r'''/r − 3 r'r''/r² + 2 (r'/r)³) Birge & Kärtner
839
+ *
840
+ * GD comes out in the reciprocal of the caller's ω unit, GDD in its square and
841
+ * TOD in its cube. */
842
+
843
+ static void jphase(jet coefficient, double *out5) {
844
+ cx d[4];
845
+ jderivs(coefficient, d);
846
+ cx value = d[0];
847
+ double magnitudeSquared = value.re * value.re + value.im * value.im;
848
+ if (magnitudeSquared == 0.0 || !isfinite(magnitudeSquared)) {
849
+ for (int i = 0; i < 5; i++) out5[i] = NAN;
850
+ return;
851
+ }
852
+ cx inverse = cdiv(cmk(1.0, 0.0), value);
853
+ cx firstRatio = cmul(d[1], inverse);
854
+ cx secondRatio = cmul(d[2], inverse);
855
+ cx thirdRatio = cmul(d[3], inverse);
856
+ cx squareFirst = cmk(firstRatio.re * firstRatio.re - firstRatio.im * firstRatio.im,
857
+ 2.0 * firstRatio.re * firstRatio.im);
858
+ cx firstTimesSecond = cmk(
859
+ firstRatio.re * secondRatio.re - firstRatio.im * secondRatio.im,
860
+ firstRatio.re * secondRatio.im + firstRatio.im * secondRatio.re);
861
+ cx cubeFirst = cmk(squareFirst.re * firstRatio.re - squareFirst.im * firstRatio.im,
862
+ squareFirst.re * firstRatio.im + squareFirst.im * firstRatio.re);
863
+ out5[0] = -atan2(value.im, value.re);
864
+ out5[1] = firstRatio.im;
865
+ out5[2] = secondRatio.im - squareFirst.im;
866
+ out5[3] = thirdRatio.im - 3.0 * firstTimesSecond.im + 2.0 * cubeFirst.im;
867
+ out5[4] = magnitudeSquared;
868
+ }
869
+
870
+ /* ── Phase core: one wavelength, both coefficients ───────────────────────────
871
+ * `sinJet` is the incident-side sine as a jet, for a stack embedded in a
872
+ * dispersive medium at a fixed external angle; NULL uses the constant
873
+ * sin(theta_deg). out10 = [r: phaseRad, GD, GDD, TOD, |r|²][t: same]. */
874
+
875
+ static void jphase_core(double lambda, double omega, double theta_deg, int pol,
876
+ jet n0, jet ns, const jet *layerN, const double *thick, int N,
877
+ const jet *sinJet, double *out10) {
878
+ jet wavelength = jwavelength(lambda, omega);
879
+ jet incidentSine = sinJet ? *sinJet : jconst(sin(theta_deg * PI / 180.0), 0.0);
880
+ jet incidentCosine = sinJet
881
+ ? jsqrt_j(jsub(jconst(1.0, 0.0), jmul(incidentSine, incidentSine)))
882
+ : jconst(cos(theta_deg * PI / 180.0), 0.0);
883
+ jet incidentEta = jadmittance(n0, incidentCosine, pol);
884
+ jet substrateCosine = jsnell_cos(n0, incidentSine, ns);
885
+ jet substrateEta = jadmittance(ns, substrateCosine, pol);
886
+
887
+ jmat2 M = jidentity();
888
+ double logScale = 0.0;
889
+ for (int k = 0; k < N; k++) {
890
+ if (!(thick[k] > 0.0)) continue;
891
+ jet cosine = jsnell_cos(n0, incidentSine, layerN[k]);
892
+ M = jmatmul(M, jlayer_matrix(layerN[k], thick[k], wavelength, cosine, pol));
893
+ logScale += jrescale(&M, MATRIX_RESCALE_THRESHOLD);
894
+ }
895
+ jcoef coefficients = jcoef_from_matrix(M, incidentEta, substrateEta, logScale);
896
+ jphase(coefficients.reflection, &out10[0]);
897
+ jphase(coefficients.transmission, &out10[5]);
898
+ }
899
+
900
+ /* ── Exported: phase dispersion at one wavelength ─────────────────────────────
901
+ * Mirrors tmmPhaseDispersion() in phase.js. Index jets are 8 doubles each,
902
+ * [re,im] per order. `sinJet` may be NULL. */
903
+
904
+ TMM_EXPORT("tmm_phase_one")
905
+ void tmm_phase_one(double lambda, double omega, double theta_deg, int pol,
906
+ const double *n0jet, const double *nsjet,
907
+ const double *layerJets, const double *thick, int N,
908
+ const double *sinJet, double *out) {
909
+ jet *layerN = (jet *)malloc(sizeof(jet) * (N > 0 ? N : 1));
910
+ for (int k = 0; k < N; k++) layerN[k] = jread(&layerJets[8 * k]);
911
+ jet sine;
912
+ if (sinJet) sine = jread(sinJet);
913
+ jphase_core(lambda, omega, theta_deg, pol,
914
+ jread(n0jet), jread(nsjet), layerN, thick, N,
915
+ sinJet ? &sine : NULL, out);
916
+ free(layerN);
917
+ }
918
+
919
+ /* ── Exported: batched phase dispersion over a wavelength grid ────────────────
920
+ * One call evaluates the whole grid, amortizing the JS↔WASM boundary the same
921
+ * way tmm_spectrum does. Polarization is an argument rather than both-at-once,
922
+ * because this kernel is an order of magnitude dearer per sample than the plain
923
+ * spectrum and callers at normal incidence would pay twice for nothing.
924
+ *
925
+ * Memory layout (all f64, caller-owned):
926
+ * lambdas : nLam
927
+ * omegas : nLam angular frequency per λ; sets the unit
928
+ * n0jets : nLam × 8 incident-medium index jet per λ
929
+ * nsjets : nLam × 8 substrate index jet per λ
930
+ * matJets : N × nLam × 8 per-layer index jet, layout [layer][λ]
931
+ * thick : N
932
+ * sinJets : nLam × 8, or NULL
933
+ * out : nLam × 10 [r: phaseRad,GD,GDD,TOD,|r|²][t: same] */
934
+
935
+ TMM_EXPORT("tmm_phase_spectrum")
936
+ void tmm_phase_spectrum(const double *lambdas, const double *omegas, int nLam,
937
+ const double *n0jets, const double *nsjets,
938
+ const double *matJets, const double *thick, int N,
939
+ double theta_deg, int pol,
940
+ const double *sinJets, double *out) {
941
+ jet *layerN = (jet *)malloc(sizeof(jet) * (N > 0 ? N : 1));
942
+ for (int li = 0; li < nLam; li++) {
943
+ for (int k = 0; k < N; k++) {
944
+ long base = ((long)k * nLam + li) * 8;
945
+ layerN[k] = jread(&matJets[base]);
946
+ }
947
+ jet sine;
948
+ if (sinJets) sine = jread(&sinJets[8 * (long)li]);
949
+ jphase_core(lambdas[li], omegas[li], theta_deg, pol,
950
+ jread(&n0jets[8 * (long)li]), jread(&nsjets[8 * (long)li]),
951
+ layerN, thick, N,
952
+ sinJets ? &sine : NULL, &out[10 * (long)li]);
953
+ }
954
+ free(layerN);
955
+ }
956
+
957
+ /* ── Exported: phase dispersion plus exact thickness derivatives ──────────────
958
+ * Mirrors tmmPhaseThicknessJacobian() in phase.js. Frequency stays the Taylor
959
+ * variable, so each thickness derivative is itself a third-order frequency jet.
960
+ * Zero-thickness layers are retained so derivative indices line up with the
961
+ * caller's design array. Negative and NaN thicknesses are skipped with a zero
962
+ * derivative, matching the point evaluator's base result.
963
+ *
964
+ * out : 10 as tmm_phase_one
965
+ * deriv : 8 × N [side][quantity][layer], side 0 = r, 1 = t,
966
+ * quantity 0 = dPhaseDeg, 1 = dGD, 2 = dGDD, 3 = dTOD
967
+ *
968
+ * The prefix/suffix decomposition cannot carry a rescaling, so if the matrix
969
+ * product overflows, `out` is still filled from the plain path and every entry
970
+ * of `deriv` is set to NaN. */
971
+
972
+ TMM_EXPORT("tmm_phase_jacobian")
973
+ void tmm_phase_jacobian(double lambda, double omega, double theta_deg, int pol,
974
+ const double *n0jet, const double *nsjet,
975
+ const double *layerJets, const double *thick, int N,
976
+ const double *sinJet, double *out, double *deriv) {
977
+ jet n0 = jread(n0jet), ns = jread(nsjet);
978
+ jet *layerN = (jet *)malloc(sizeof(jet) * (N > 0 ? N : 1));
979
+ for (int k = 0; k < N; k++) layerN[k] = jread(&layerJets[8 * k]);
980
+ jet sine;
981
+ if (sinJet) sine = jread(sinJet);
982
+ const jet *sinePtr = sinJet ? &sine : NULL;
983
+
984
+ jet wavelength = jwavelength(lambda, omega);
985
+ jet incidentSine = sinePtr ? *sinePtr : jconst(sin(theta_deg * PI / 180.0), 0.0);
986
+ jet incidentCosine = sinePtr
987
+ ? jsqrt_j(jsub(jconst(1.0, 0.0), jmul(incidentSine, incidentSine)))
988
+ : jconst(cos(theta_deg * PI / 180.0), 0.0);
989
+ jet incidentEta = jadmittance(n0, incidentCosine, pol);
990
+ jet substrateCosine = jsnell_cos(n0, incidentSine, ns);
991
+ jet substrateEta = jadmittance(ns, substrateCosine, pol);
992
+
993
+ int M = (N > 0 ? N : 1);
994
+ jmat2 *layerM = (jmat2 *)malloc(sizeof(jmat2) * M);
995
+ jmat2 *layerDM = (jmat2 *)malloc(sizeof(jmat2) * M);
996
+ for (int k = 0; k < N; k++) {
997
+ /* Match jphase_core's skip rule for invalid negative/NaN thicknesses,
998
+ * while retaining the useful derivative of a zero-thickness layer. */
999
+ if (!(thick[k] >= 0.0)) {
1000
+ layerM[k] = jidentity();
1001
+ layerDM[k] = jzero();
1002
+ continue;
1003
+ }
1004
+ jet cosine = jsnell_cos(n0, incidentSine, layerN[k]);
1005
+ jlayer_matrix_dd(layerN[k], thick[k], wavelength, cosine, pol,
1006
+ &layerM[k], &layerDM[k]);
1007
+ }
1008
+
1009
+ jmat2 *prefix = (jmat2 *)malloc(sizeof(jmat2) * (N + 1));
1010
+ jmat2 *suffix = (jmat2 *)malloc(sizeof(jmat2) * (N + 1));
1011
+ prefix[0] = jidentity();
1012
+ int overflowed = 0;
1013
+ for (int k = 0; k < N; k++) {
1014
+ prefix[k + 1] = jmatmul(prefix[k], layerM[k]);
1015
+ if (jmatmag(prefix[k + 1]) > MATRIX_RESCALE_THRESHOLD) { overflowed = 1; break; }
1016
+ }
1017
+
1018
+ if (overflowed) {
1019
+ jphase_core(lambda, omega, theta_deg, pol, n0, ns, layerN, thick, N, sinePtr, out);
1020
+ for (long i = 0; i < 8L * N; i++) deriv[i] = NAN;
1021
+ free(layerN); free(layerM); free(layerDM); free(prefix); free(suffix);
1022
+ return;
1023
+ }
1024
+
1025
+ suffix[N] = jidentity();
1026
+ for (int k = N - 1; k >= 0; k--) suffix[k] = jmatmul(layerM[k], suffix[k + 1]);
1027
+
1028
+ jcoef coefficients = jcoef_from_matrix(prefix[N], incidentEta, substrateEta, 0.0);
1029
+ jphase(coefficients.reflection, &out[0]);
1030
+ jphase(coefficients.transmission, &out[5]);
1031
+
1032
+ for (int k = 0; k < N; k++) {
1033
+ jmat2 matrixDerivative = jmatmul(jmatmul(prefix[k], layerDM[k]), suffix[k + 1]);
1034
+ jet dReflection, dTransmission;
1035
+ jcoef_thickness(matrixDerivative, coefficients, incidentEta, substrateEta,
1036
+ &dReflection, &dTransmission);
1037
+ const jet sides[2] = { dReflection, dTransmission };
1038
+ const jet base[2] = { coefficients.reflection, coefficients.transmission };
1039
+ for (int s = 0; s < 2; s++) {
1040
+ cx dd[4];
1041
+ jderivs(jdiv(sides[s], base[s]), dd);
1042
+ deriv[((long)s * 4 + 0) * N + k] = -dd[0].im * 180.0 / PI;
1043
+ deriv[((long)s * 4 + 1) * N + k] = dd[1].im;
1044
+ deriv[((long)s * 4 + 2) * N + k] = dd[2].im;
1045
+ deriv[((long)s * 4 + 3) * N + k] = dd[3].im;
1046
+ }
1047
+ }
1048
+
1049
+ free(layerN); free(layerM); free(layerDM); free(prefix); free(suffix);
1050
+ }
Binary file