tmmcore 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmmcore",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Transfer-matrix method for multilayer thin-film optics, with exact analytic derivatives and phase dispersion. JavaScript, C and WebAssembly.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -20,10 +20,10 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
- "test": "node tests/equivalence.mjs && node tests/growing_equivalence.mjs && node tests/growing_eval_equivalence.mjs",
23
+ "test": "node tests/equivalence.mjs && node tests/growing_equivalence.mjs && node tests/growing_eval_equivalence.mjs && node tests/absorbing_incident.mjs",
24
24
  "compare": "node benchmarks/compare.mjs",
25
25
  "examples": "node examples/01-single-layer.mjs && node examples/02-ar-coating.mjs && node examples/03-metal-mirror.mjs && node examples/04-refine.mjs && node examples/05-angle-map.mjs && node examples/06-needle.mjs && node examples/07-jacobian-map.mjs && node examples/08-group-delay.mjs",
26
- "build:wasm": "emcc src/tmm_kernel.c -O3 --no-entry -sSTANDALONE_WASM=1 -sALLOW_MEMORY_GROWTH=1 -sEXPORTED_FUNCTIONS=_tmm_one,_tmm_spectrum,_tmm_jacobian,_tmm_needle_scan,_tmm_hessian,_tmm_phase_one,_tmm_phase_spectrum,_tmm_phase_jacobian,_tmm_monitor_curve,_tmm_deposition_spectra,_tmm_growing_eval_create,_tmm_growing_eval_set_top,_tmm_growing_eval_sample,_tmm_growing_eval_free,_malloc,_free -o src/tmm_kernel.wasm",
26
+ "build:wasm": "emcc src/tmm_kernel.c -O3 --no-entry -sSTANDALONE_WASM=1 -sALLOW_MEMORY_GROWTH=1 -sEXPORTED_FUNCTIONS=_tmm_one,_tmm_spectrum,_tmm_jacobian,_tmm_needle_scan,_tmm_hessian,_tmm_phase_one,_tmm_phase_spectrum,_tmm_phase_jacobian,_tmm_phase_jacobian_spectrum,_tmm_monitor_curve,_tmm_deposition_spectra,_tmm_growing_eval_create,_tmm_growing_eval_set_top,_tmm_growing_eval_sample,_tmm_growing_eval_free,_malloc,_free -o src/tmm_kernel.wasm",
27
27
  "build:native": "cc -std=c99 -pedantic -Wall -Wextra -O2 -c src/tmm_kernel.c -o tmm_kernel.o"
28
28
  },
29
29
  "keywords": [
package/src/index.js CHANGED
@@ -29,7 +29,7 @@ export {
29
29
  tmmNeedleScan,
30
30
  // Low-level primitives, for building variants on the same conventions
31
31
  cadd, csub, cmul, cdiv, cabs2, cconj, csqrt, ccos, csin, creal, cimag,
32
- matmul, rescaleMatrix, snellCosTheta, layerMatrix, cmatvec,
32
+ matmul, rescaleMatrix, snellCosTheta, incidentCosTheta, layerMatrix, cmatvec,
33
33
  } from './tmm.js';
34
34
 
35
35
  export {
package/src/phase.js CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  jetDerivatives,
47
47
  jetDivide,
48
48
  jetMultiply,
49
+ jetRealPart,
49
50
  jetScale,
50
51
  jetSinCos,
51
52
  jetSqrt,
@@ -124,11 +125,25 @@ function rescaleMatrix(matrix, threshold) {
124
125
  return Math.log(scale);
125
126
  }
126
127
 
128
+ // The transverse invariant is Re(n0) sinθ0, as in tmm.js: an absorbing incident
129
+ // medium carries a wave whose amplitude falls along the normal only.
127
130
  function snellCosine(incidentIndex, incidentSine, layerIndex) {
128
- const layerSine = jetDivide(jetMultiply(incidentIndex, incidentSine), layerIndex);
131
+ const layerSine = jetDivide(jetMultiply(jetRealPart(incidentIndex), incidentSine), layerIndex);
129
132
  return jetSqrt(jetSubtract(jetConstant(1), jetMultiply(layerSine, layerSine)));
130
133
  }
131
134
 
135
+ // cosθ0 of the incident medium: the plain cosine while the index is real at
136
+ // every order, otherwise from the same real invariant as the layers, so the
137
+ // incident admittance is sqrt(N0² − n0² sin²θ0) and matches tmm.js.
138
+ function incidentCosine(incidentIndex, incidentSine, incidentSineJet, thetaDeg) {
139
+ if (incidentIndex.some(coefficient => coefficient[1] !== 0)) {
140
+ return snellCosine(incidentIndex, incidentSine, incidentIndex);
141
+ }
142
+ return incidentSineJet
143
+ ? jetSqrt(jetSubtract(jetConstant(1), jetMultiply(incidentSine, incidentSine)))
144
+ : jetConstant(Math.cos(thetaDeg * Math.PI / 180));
145
+ }
146
+
132
147
  function admittance(index, cosine, polarization) {
133
148
  return polarization === 's'
134
149
  ? jetMultiply(index, cosine)
@@ -249,10 +264,8 @@ export function tmmCoefficientJets({
249
264
  layers,
250
265
  }) {
251
266
  const incidentSine = incidentSineJet || jetConstant(Math.sin(thetaDeg * Math.PI / 180));
252
- const incidentCosine = incidentSineJet
253
- ? jetSqrt(jetSubtract(jetConstant(1), jetMultiply(incidentSine, incidentSine)))
254
- : jetConstant(Math.cos(thetaDeg * Math.PI / 180));
255
- const incidentEta = admittance(incidentIndexJet, incidentCosine, polarization);
267
+ const incidentEta = admittance(incidentIndexJet,
268
+ incidentCosine(incidentIndexJet, incidentSine, incidentSineJet, thetaDeg), polarization);
256
269
  const substrateCosine = snellCosine(incidentIndexJet, incidentSine, substrateIndexJet);
257
270
  const substrateEta = admittance(substrateIndexJet, substrateCosine, polarization);
258
271
 
@@ -301,10 +314,8 @@ export function tmmCoefficientThicknessJets(options) {
301
314
  layers,
302
315
  } = options;
303
316
  const incidentSine = incidentSineJet || jetConstant(Math.sin(thetaDeg * Math.PI / 180));
304
- const incidentCosine = incidentSineJet
305
- ? jetSqrt(jetSubtract(jetConstant(1), jetMultiply(incidentSine, incidentSine)))
306
- : jetConstant(Math.cos(thetaDeg * Math.PI / 180));
307
- const incidentEta = admittance(incidentIndexJet, incidentCosine, polarization);
317
+ const incidentEta = admittance(incidentIndexJet,
318
+ incidentCosine(incidentIndexJet, incidentSine, incidentSineJet, thetaDeg), polarization);
308
319
  const substrateCosine = snellCosine(incidentIndexJet, incidentSine, substrateIndexJet);
309
320
  const substrateEta = admittance(substrateIndexJet, substrateCosine, polarization);
310
321
  const layerData = layers.map((layer) => {
@@ -419,14 +430,17 @@ export function coefficientPhaseDispersion(coefficientJet) {
419
430
  */
420
431
  export function coefficientPhaseThicknessDerivatives(coefficientJet, thicknessJets) {
421
432
  if (!thicknessJets) return null;
422
- const result = { phaseDeg: [], gd: [], gdd: [], tod: [] };
433
+ const result = { phaseDeg: [], gd: [], gdd: [], tod: [], logMagnitudeSquared: [] };
423
434
  for (const thicknessJet of thicknessJets) {
424
435
  const logarithmicDerivative = jetDivide(thicknessJet, coefficientJet);
425
436
  const derivatives = jetDerivatives(logarithmicDerivative);
437
+ // The imaginary part of d(ln c)/dd is the phase derivative; the real
438
+ // part is d(ln |c|)/dd, so twice it is the relative derivative of |c|².
426
439
  result.phaseDeg.push(-derivatives[0][1] * 180 / Math.PI);
427
440
  result.gd.push(derivatives[1][1]);
428
441
  result.gdd.push(derivatives[2][1]);
429
442
  result.tod.push(derivatives[3][1]);
443
+ result.logMagnitudeSquared.push(2 * derivatives[0][0]);
430
444
  }
431
445
  return result;
432
446
  }
@@ -489,8 +503,10 @@ export function tmmPhaseDispersion(lambda_nm, theta_deg, pol, n0Jet, nsJet, laye
489
503
  * thicknesses are skipped and receive zero derivative entries.
490
504
  *
491
505
  * @returns {{r, t}} where each side is the phase quantities plus
492
- * `{dPhaseDeg, dGd, dGdd, dTod}`, arrays of length `layers.length`. The
493
- * derivative arrays are `null` if the matrix product overflowed.
506
+ * `{dPhaseDeg, dGd, dGdd, dTod, dLogMagnitudeSquared}`, arrays of length
507
+ * `layers.length`. `dLogMagnitudeSquared` is d(ln |coefficient|²)/dd, the
508
+ * relative intensity derivative. The derivative arrays are `null` if the
509
+ * matrix product overflowed.
494
510
  */
495
511
  export function tmmPhaseThicknessJacobian(lambda_nm, theta_deg, pol, n0Jet, nsJet, layers, options = {}) {
496
512
  const prepared = prepare(lambda_nm, layers, options);
@@ -513,6 +529,7 @@ export function tmmPhaseThicknessJacobian(lambda_nm, theta_deg, pol, n0Jet, nsJe
513
529
  dGd: derivatives ? derivatives.gd : null,
514
530
  dGdd: derivatives ? derivatives.gdd : null,
515
531
  dTod: derivatives ? derivatives.tod : null,
532
+ dLogMagnitudeSquared: derivatives ? derivatives.logMagnitudeSquared : null,
516
533
  };
517
534
  };
518
535
  return {
package/src/taylorJet.js CHANGED
@@ -233,6 +233,11 @@ export function jetWithImaginaryPart(realJet, imaginaryJet) {
233
233
  return realJet.map((coefficient, index) => [coefficient[0], imaginaryJet[index][0]]);
234
234
  }
235
235
 
236
+ /** The real part of a complex jet, order by order. */
237
+ export function jetRealPart(jet) {
238
+ return jet.map(coefficient => [coefficient[0], 0]);
239
+ }
240
+
236
241
  /** Floor the value at `minimum`, flattening the jet to a constant when it bites. */
237
242
  export function jetClampRealMinimum(jet, minimum) {
238
243
  return jet[0][0] >= minimum ? jet : jetConstant(minimum);
package/src/tmm.js CHANGED
@@ -20,7 +20,9 @@
20
20
  * it to float64 round-off (see tests/).
21
21
  *
22
22
  * References:
23
- * • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125
23
+ * • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125;
24
+ * Eq. 2.83 (transmittance out of an absorbing incident medium);
25
+ * §10.2, Eqs. 10.11–10.13 (tilted admittances from the real invariant n0 sinθ0)
24
26
  * • Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996), Eqs. (3)–(6)
25
27
  * • Tikhonravov, Trubetskov & DeBell, Appl. Opt. 35, 5493 (1996)
26
28
  */
@@ -90,14 +92,37 @@ function rescaleMatrix(M) {
90
92
  }
91
93
 
92
94
  // ── Snell's law ───────────────────────────────────────────────────────────────
95
+ //
96
+ // Every wave in the stack shares one transverse wavevector, n0 sinθ0 with n0
97
+ // the REAL part of the incident index (Macleod 5th ed., §10.2, Eqs. 10.11 to
98
+ // 10.13). An absorbing incident medium then carries an inhomogeneous wave whose
99
+ // amplitude falls along the normal only, and each medium's cosθ follows from
100
+ // that one real invariant. Putting the complex index into the invariant would
101
+ // make the amplitude vary along the interface, so energy would flow sideways
102
+ // inside lossless layers and R + T would be wrong by a term linear in the
103
+ // incident k at oblique incidence.
93
104
 
94
105
  function snellCosTheta(n0, sinTheta0, nj) {
95
- // sinThetaJ = n0 * sinTheta0 / nj (complex)
96
- const sinThetaJ = cdiv(cmul(n0, sinTheta0), nj);
106
+ // sinThetaJ = Re(n0) * sinTheta0 / nj (complex)
107
+ const sinThetaJ = cdiv(cmul([n0[0], 0], sinTheta0), nj);
97
108
  // cosTheta = sqrt(1 - sin²θ)
98
109
  return csqrt(csub([1, 0], cmul(sinThetaJ, sinThetaJ)));
99
110
  }
100
111
 
112
+ // cosθ0 of the incident medium. A transparent medium keeps the plain
113
+ // sqrt(1 − sin²θ0). An absorbing one takes its cosine from the same real
114
+ // invariant as the layers, so its tilted admittance is sqrt(N0² − n0² sin²θ0)
115
+ // and the incident wave belongs to the same boundary problem as the rest of
116
+ // the stack. R + T then departs from 1 for lossless layers only by the
117
+ // interference of the incident and reflected waves in the absorbing medium,
118
+ // the term Macleod's Eq. 2.83 carries, which is second order in k0 for a
119
+ // bare interface.
120
+ function incidentCosTheta(n0, sinTheta0) {
121
+ return n0[1] === 0
122
+ ? csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)))
123
+ : snellCosTheta(n0, sinTheta0, n0);
124
+ }
125
+
101
126
  // ── Layer characteristic matrix ───────────────────────────────────────────────
102
127
 
103
128
  function layerMatrix(nj, dj_nm, lambda_nm, cosTheta_j, pol) {
@@ -150,7 +175,7 @@ function layerMatrix(nj, dj_nm, lambda_nm, cosTheta_j, pol) {
150
175
  */
151
176
  export function tmm(lambda_nm, theta_deg, pol, n0, ns, layers) {
152
177
  const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
153
- const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
178
+ const cosTheta0 = incidentCosTheta(n0, sinTheta0);
154
179
 
155
180
  // Admittance of incident medium
156
181
  const eta0 = pol === 's'
@@ -241,7 +266,7 @@ function cmatvec(M, v) {
241
266
  export function tmmNeedleScan(lambda_nm, theta_deg, pol, n0, ns, layers,
242
267
  candidateNs, intraFracs = []) {
243
268
  const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
244
- const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
269
+ const cosTheta0 = incidentCosTheta(n0, sinTheta0);
245
270
  const eta0 = pol === 's' ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
246
271
  const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
247
272
  const etaS = pol === 's' ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
@@ -368,7 +393,7 @@ export function tmmNeedleScan(lambda_nm, theta_deg, pol, n0, ns, layers,
368
393
  // `metrics()` in tmmNeedleScan.
369
394
  export function tmmThicknessJacobian(lambda_nm, theta_deg, pol, n0, ns, layers) {
370
395
  const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
371
- const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
396
+ const cosTheta0 = incidentCosTheta(n0, sinTheta0);
372
397
  const eta0 = pol === 's' ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
373
398
  const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
374
399
  const etaS = pol === 's' ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
@@ -464,7 +489,7 @@ export function tmmThicknessJacobian(lambda_nm, theta_deg, pol, n0, ns, layers)
464
489
  // (tests/hessian_fd_validation.mjs).*
465
490
  export function tmmThicknessHessian(lambda_nm, theta_deg, pol, n0, ns, layers) {
466
491
  const sinTheta0 = [Math.sin(theta_deg * Math.PI / 180), 0];
467
- const cosTheta0 = csqrt(csub([1, 0], cmul(sinTheta0, sinTheta0)));
492
+ const cosTheta0 = incidentCosTheta(n0, sinTheta0);
468
493
  const eta0 = pol === 's' ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
469
494
  const cosThetaS = snellCosTheta(n0, sinTheta0, ns);
470
495
  const etaS = pol === 's' ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
@@ -598,5 +623,5 @@ export function tmmThicknessHessian(lambda_nm, theta_deg, pol, n0, ns, layers) {
598
623
 
599
624
  export {
600
625
  cadd, csub, cmul, cdiv, cabs2, cconj, csqrt, ccos, csin, creal, cimag,
601
- matmul, rescaleMatrix, snellCosTheta, layerMatrix, cmatvec
626
+ matmul, rescaleMatrix, snellCosTheta, incidentCosTheta, layerMatrix, cmatvec
602
627
  };