tmmcore 0.2.0 → 0.3.1

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.2.0",
3
+ "version": "0.3.1",
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",
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,_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_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/build.ps1 CHANGED
@@ -111,7 +111,7 @@ $emccArgs = @(
111
111
  '--no-entry'
112
112
  '-sSTANDALONE_WASM=1'
113
113
  '-sALLOW_MEMORY_GROWTH=1'
114
- '-sEXPORTED_FUNCTIONS=_tmm_one,_tmm_spectrum,_tmm_jacobian,_tmm_needle_scan,_tmm_hessian,_tmm_phase_one,_tmm_phase_spectrum,_tmm_phase_jacobian,_malloc,_free'
114
+ '-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'
115
115
  '-o'
116
116
  'src/tmm_kernel.wasm'
117
117
  )
@@ -121,3 +121,7 @@ if ($LASTEXITCODE -ne 0) { Write-Error "emcc failed with exit code $LASTEXITCODE
121
121
  Write-Host "Built src/tmm_kernel.wasm"
122
122
  node tests/equivalence.mjs
123
123
  if ($LASTEXITCODE -ne 0) { Write-Error "equivalence tests failed with exit code $LASTEXITCODE."; exit 1 }
124
+ node tests/growing_equivalence.mjs
125
+ if ($LASTEXITCODE -ne 0) { Write-Error "growing_equivalence tests failed with exit code $LASTEXITCODE."; exit 1 }
126
+ node tests/growing_eval_equivalence.mjs
127
+ if ($LASTEXITCODE -ne 0) { Write-Error "growing_eval_equivalence tests failed with exit code $LASTEXITCODE."; exit 1 }
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) => {
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
  };
package/src/tmmWasm.js CHANGED
@@ -59,6 +59,15 @@ function wasmImports() {
59
59
  };
60
60
  }
61
61
 
62
+ // Backstop for growing-evaluator handles that are dropped without free():
63
+ // reclaims their kernel memory when the JS handle is collected. Deterministic
64
+ // free() remains the contract; this only keeps a leak from being permanent.
65
+ const growingEvalFinalizer = typeof FinalizationRegistry !== 'undefined'
66
+ ? new FinalizationRegistry(({ wasm, ptr }) => {
67
+ try { wasm._growing_eval_free(ptr); } catch (_) { /* instance gone */ }
68
+ })
69
+ : null;
70
+
62
71
  export class TmmWasmInstance {
63
72
  constructor(instance) {
64
73
  const ex = instance.exports;
@@ -82,6 +91,16 @@ export class TmmWasmInstance {
82
91
  this._tmm_phase_one = ex.tmm_phase_one || ex._tmm_phase_one || null;
83
92
  this._tmm_phase_spectrum = ex.tmm_phase_spectrum || ex._tmm_phase_spectrum || null;
84
93
  this._tmm_phase_jacobian = ex.tmm_phase_jacobian || ex._tmm_phase_jacobian || null;
94
+ // Optional, same reason: the growing-stack kernels (monitor curve and
95
+ // per-step deposition spectra) arrived after all of the above.
96
+ this._tmm_monitor_curve = ex.tmm_monitor_curve || ex._tmm_monitor_curve || null;
97
+ this._tmm_deposition_spectra = ex.tmm_deposition_spectra || ex._tmm_deposition_spectra || null;
98
+ // Optional, same reason: the persistent growing-layer evaluator
99
+ // (wavelength-grid-per-call) arrived after the two kernels above.
100
+ this._growing_eval_create = ex.tmm_growing_eval_create || ex._tmm_growing_eval_create || null;
101
+ this._growing_eval_set_top = ex.tmm_growing_eval_set_top || ex._tmm_growing_eval_set_top || null;
102
+ this._growing_eval_sample = ex.tmm_growing_eval_sample || ex._tmm_growing_eval_sample || null;
103
+ this._growing_eval_free = ex.tmm_growing_eval_free || ex._tmm_growing_eval_free || null;
85
104
  const missingExports = !this.malloc || !this.free || !this._tmm_one ||
86
105
  !this._tmm_spectrum || !this._tmm_jacobian || !this._tmm_needle_scan;
87
106
  if (missingExports) {
@@ -192,6 +211,234 @@ export class TmmWasmInstance {
192
211
  return res;
193
212
  }
194
213
 
214
+ /** True if the loaded module carries the growing-stack kernels. */
215
+ hasGrowingKernels() {
216
+ return !!(this._tmm_monitor_curve && this._tmm_deposition_spectra);
217
+ }
218
+
219
+ /** True if the loaded module carries the persistent growing-layer evaluator. */
220
+ hasGrowingEval() {
221
+ return !!(this._growing_eval_create && this._growing_eval_set_top
222
+ && this._growing_eval_sample && this._growing_eval_free);
223
+ }
224
+
225
+ /**
226
+ * Monitor curve of one growing layer at one wavelength: the completed
227
+ * stack's matrix is built once, then every sample thickness costs one 2×2
228
+ * multiply. Returns the forward and substrate-side passes of the coated
229
+ * surface, both polarizations; the caller does the incoherent slab
230
+ * combination (or forms A = 1−R−T for a semi-infinite substrate).
231
+ * @param {[number,number]} n0 incident ñ
232
+ * @param {[number,number]} ns substrate ñ
233
+ * @param {{n:[number,number],d:number}[]} baseLayers completed stack,
234
+ * outermost first
235
+ * @param {[number,number]} ngNK growing layer ñ
236
+ * @param {ArrayLike<number>} dArr sample thicknesses (nm)
237
+ * @returns {{Rs,Ts,Rp,Tp,Rrs,Rrp}} each a Float64Array(dArr.length)
238
+ */
239
+ monitorCurve(lambda_nm, theta_deg, n0, ns, baseLayers, ngNK, dArr) {
240
+ const NB = baseLayers.length;
241
+ const nD = dArr.length;
242
+ // arena: base[3NB] | d[nD] | Rs,Ts,Rp,Tp,Rrs,Rrp [6·nD]
243
+ const oBase = 0, oD = 3 * NB, oOut = oD + nD;
244
+ const need = oOut + 6 * nD;
245
+ const ptr = this._scratch(need);
246
+ const buf = this._view(ptr, need);
247
+ for (let i = 0; i < NB; i++) {
248
+ buf[3 * i + 0] = baseLayers[i].n[0];
249
+ buf[3 * i + 1] = baseLayers[i].n[1];
250
+ buf[3 * i + 2] = baseLayers[i].d;
251
+ }
252
+ for (let k = 0; k < nD; k++) buf[oD + k] = dArr[k];
253
+ const P = (off) => ptr + off * 8;
254
+ this._tmm_monitor_curve(lambda_nm, theta_deg,
255
+ n0[0], n0[1], ns[0], ns[1], P(oBase), NB, ngNK[0], ngNK[1],
256
+ P(oD), nD,
257
+ P(oOut), P(oOut + nD), P(oOut + 2 * nD), P(oOut + 3 * nD),
258
+ P(oOut + 4 * nD), P(oOut + 5 * nD));
259
+ // Fresh view after the call: memory growth detaches the old buffer.
260
+ const out = this._view(ptr, need);
261
+ return {
262
+ Rs: out.slice(oOut, oOut + nD),
263
+ Ts: out.slice(oOut + nD, oOut + 2 * nD),
264
+ Rp: out.slice(oOut + 2 * nD, oOut + 3 * nD),
265
+ Tp: out.slice(oOut + 3 * nD, oOut + 4 * nD),
266
+ Rrs: out.slice(oOut + 4 * nD, oOut + 5 * nD),
267
+ Rrp: out.slice(oOut + 5 * nD, oOut + 6 * nD),
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Per-step spectra of a growing stack: one call returns the forward and
273
+ * substrate-side passes of the coated surface after every deposited layer,
274
+ * so all N step spectra cost about what the final one costs alone. Layers
275
+ * in DEPOSITION order (first deposited first).
276
+ * @param {number[]} lambdas
277
+ * @param {[number,number][]} n0List incident ñ per λ
278
+ * @param {[number,number][]} nsList substrate ñ per λ
279
+ * @param {[number,number][][]} layerNK [layer][λ] = ñ, deposition order
280
+ * @param {number[]} thick thicknesses (nm); 0 repeats the previous step
281
+ * @param {number} theta_deg
282
+ * @returns {{Rs,Ts,Rp,Tp,Rrs,Rrp}} each a Float64Array(N · nLam),
283
+ * step-major: value for step k at wavelength i is at [k·nLam + i]
284
+ */
285
+ depositionSpectra(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
286
+ const nLam = lambdas.length;
287
+ const N = thick.length;
288
+ const nOut = Math.max(1, N * nLam);
289
+
290
+ const lamPtr = this._alloc(nLam);
291
+ const n0Ptr = this._alloc(2 * nLam);
292
+ const nsPtr = this._alloc(2 * nLam);
293
+ const mPtr = this._alloc(Math.max(1, 2 * N * nLam));
294
+ const thPtr = this._alloc(Math.max(1, N));
295
+ const outPtrs = Array.from({ length: 6 }, () => this._alloc(nOut));
296
+
297
+ const lam = this._view(lamPtr, nLam);
298
+ const n0v = this._view(n0Ptr, 2 * nLam);
299
+ const nsv = this._view(nsPtr, 2 * nLam);
300
+ const mv = this._view(mPtr, Math.max(1, 2 * N * nLam));
301
+ const thv = this._view(thPtr, Math.max(1, N));
302
+ for (let i = 0; i < nLam; i++) {
303
+ lam[i] = lambdas[i];
304
+ n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
305
+ nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
306
+ }
307
+ for (let k = 0; k < N; k++) {
308
+ thv[k] = thick[k];
309
+ const row = layerNK[k];
310
+ const base = k * nLam * 2;
311
+ for (let i = 0; i < nLam; i++) {
312
+ mv[base + 2 * i] = row[i][0];
313
+ mv[base + 2 * i + 1] = row[i][1];
314
+ }
315
+ }
316
+
317
+ const ok = this._tmm_deposition_spectra(lamPtr, nLam, n0Ptr, nsPtr, mPtr, thPtr, N,
318
+ theta_deg, ...outPtrs);
319
+ if (!ok) {
320
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr, ...outPtrs]) this.free(p);
321
+ // The outputs were never written; returning them would hand the
322
+ // caller uninitialized memory as spectra.
323
+ throw new Error('tmmWasm: deposition-spectra state allocation failed');
324
+ }
325
+
326
+ const cp = (p) => Float64Array.from(this._view(p, nOut));
327
+ const [Rs, Ts, Rp, Tp, Rrs, Rrp] = outPtrs.map(cp);
328
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr, ...outPtrs]) this.free(p);
329
+ return { Rs, Ts, Rp, Tp, Rrs, Rrp };
330
+ }
331
+
332
+ /**
333
+ * Persistent growing-layer evaluator: the completed stack's products for
334
+ * the whole wavelength grid are folded once and kept in kernel memory;
335
+ * each sample() then answers one thickness of the growing layer across
336
+ * the grid. This is the shape a broadband monitor scan needs (a spectrum
337
+ * per scan while the layers beneath stay fixed), where monitorCurve is
338
+ * the single-λ, many-thicknesses shape.
339
+ *
340
+ * The completed stack arrives OUTERMOST FIRST, layer-major like
341
+ * depositionSpectra's matNK; zero-thickness entries are skipped.
342
+ *
343
+ * The returned handle owns kernel memory: call free() when the layer is
344
+ * done. A dropped handle is reclaimed by a finalizer eventually, but
345
+ * deterministic free() is what keeps a long run's footprint flat.
346
+ *
347
+ * @param {number[]} lambdas
348
+ * @param {[number,number][]} n0List incident ñ per λ
349
+ * @param {[number,number][]} nsList substrate ñ per λ
350
+ * @param {[number,number][][]} layerNK [layer][λ] = ñ, outermost first
351
+ * @param {number[]} thick completed thicknesses (nm)
352
+ * @param {number} theta_deg
353
+ * @returns {{setTop, sample, free}} setTop(ngList) declares the growing
354
+ * layer's ñ per λ; sample(d, out?) returns {Rs,Ts,Rp,Tp,Rrs,Rrp}
355
+ * (each Float64Array(nLam), written into `out` when given, so a
356
+ * scan loop can reuse one set of buffers).
357
+ */
358
+ growingEval(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
359
+ if (!this.hasGrowingEval()) throw new Error('tmmWasm: growing evaluator kernel not in this build');
360
+ const nLam = lambdas.length;
361
+ const NB = thick.length;
362
+
363
+ const lamPtr = this._alloc(nLam);
364
+ const n0Ptr = this._alloc(2 * nLam);
365
+ const nsPtr = this._alloc(2 * nLam);
366
+ const mPtr = this._alloc(Math.max(1, 2 * NB * nLam));
367
+ const thPtr = this._alloc(Math.max(1, NB));
368
+ const lam = this._view(lamPtr, nLam);
369
+ const n0v = this._view(n0Ptr, 2 * nLam);
370
+ const nsv = this._view(nsPtr, 2 * nLam);
371
+ const mv = this._view(mPtr, Math.max(1, 2 * NB * nLam));
372
+ const thv = this._view(thPtr, Math.max(1, NB));
373
+ for (let i = 0; i < nLam; i++) {
374
+ lam[i] = lambdas[i];
375
+ n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
376
+ nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
377
+ }
378
+ for (let k = 0; k < NB; k++) {
379
+ thv[k] = thick[k];
380
+ const row = layerNK[k];
381
+ const base = k * nLam * 2;
382
+ for (let i = 0; i < nLam; i++) {
383
+ mv[base + 2 * i] = row[i][0];
384
+ mv[base + 2 * i + 1] = row[i][1];
385
+ }
386
+ }
387
+ const handlePtr = this._growing_eval_create(lamPtr, nLam, theta_deg,
388
+ n0Ptr, nsPtr, mPtr, thPtr, NB);
389
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr]) this.free(p);
390
+ if (!handlePtr) throw new Error('tmmWasm: growing evaluator allocation failed');
391
+
392
+ const self = this;
393
+ const handle = {
394
+ ptr: handlePtr,
395
+ setTop(ngList) {
396
+ if (!this.ptr) throw new Error('tmmWasm: growing evaluator already freed');
397
+ const p = self._scratch(2 * nLam);
398
+ const buf = self._view(p, 2 * nLam);
399
+ for (let i = 0; i < nLam; i++) {
400
+ buf[2 * i] = ngList[i][0]; buf[2 * i + 1] = ngList[i][1];
401
+ }
402
+ self._growing_eval_set_top(this.ptr, p);
403
+ },
404
+ sample(d, out = null) {
405
+ if (!this.ptr) throw new Error('tmmWasm: growing evaluator already freed');
406
+ const p = self._scratch(6 * nLam);
407
+ const P = (block) => p + block * nLam * 8;
408
+ const ok = self._growing_eval_sample(this.ptr, d,
409
+ P(0), P(1), P(2), P(3), P(4), P(5));
410
+ // The kernel writes nothing without a declared growing layer;
411
+ // copying the arena anyway would hand back stale memory.
412
+ if (!ok) throw new Error('tmmWasm: growing evaluator sampled before setTop');
413
+ // Fresh view AFTER the call (memory may have grown → detach).
414
+ const buf = self._view(p, 6 * nLam);
415
+ if (!out) {
416
+ out = {
417
+ Rs: new Float64Array(nLam), Ts: new Float64Array(nLam),
418
+ Rp: new Float64Array(nLam), Tp: new Float64Array(nLam),
419
+ Rrs: new Float64Array(nLam), Rrp: new Float64Array(nLam),
420
+ };
421
+ }
422
+ out.Rs.set(buf.subarray(0, nLam));
423
+ out.Ts.set(buf.subarray(nLam, 2 * nLam));
424
+ out.Rp.set(buf.subarray(2 * nLam, 3 * nLam));
425
+ out.Tp.set(buf.subarray(3 * nLam, 4 * nLam));
426
+ out.Rrs.set(buf.subarray(4 * nLam, 5 * nLam));
427
+ out.Rrp.set(buf.subarray(5 * nLam, 6 * nLam));
428
+ return out;
429
+ },
430
+ free() {
431
+ if (!this.ptr) return;
432
+ growingEvalFinalizer?.unregister(this);
433
+ self._growing_eval_free(this.ptr);
434
+ this.ptr = 0;
435
+ },
436
+ };
437
+ growingEvalFinalizer?.register(handle,
438
+ { wasm: this, ptr: handlePtr }, handle);
439
+ return handle;
440
+ }
441
+
195
442
  /**
196
443
  * Analytic thickness Jacobian for one (λ, θ, pol) : mirrors
197
444
  * tmmThicknessJacobian(). layers used AS-IS (index parity).
package/src/tmm_kernel.c CHANGED
@@ -23,7 +23,8 @@
23
23
  * Builds with any C99 compiler; no dependencies beyond libm.
24
24
  *
25
25
  * References:
26
- * • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125
26
+ * • Macleod, Thin-Film Optical Filters 5th ed., §2.4, Eqs. 2.111, 2.123–2.125;
27
+ * Eq. 2.83; §10.2, Eqs. 10.11–10.13 (tilted admittances, real invariant)
27
28
  * • Sullivan & Dobrowolski, Appl. Opt. 35, 5484 (1996), Eqs. (3)–(6)
28
29
  * • Tikhonravov, Trubetskov & DeBell, Appl. Opt. 35, 5493 (1996)
29
30
  * • Birge & Kärtner, Appl. Opt. 45, 1478 (2006) [phase dispersion]
@@ -110,13 +111,24 @@ static inline vec2 cmatvec(mat2 M, vec2 v) {
110
111
  return o;
111
112
  }
112
113
 
113
- /* ── Snell's law: cosθ_j from incident (n0, sinθ0) into medium nj ─────────── */
114
+ /* ── Snell's law: cosθ_j from incident (n0, sinθ0) into medium nj ───────────
115
+ * The transverse invariant is Re(n0) sinθ0 (Macleod 5th ed., §10.2, Eqs.
116
+ * 10.11–10.13): an absorbing incident medium carries a wave whose amplitude
117
+ * falls along the normal only, and every medium's cosθ follows from that one
118
+ * real invariant. Mirrors snellCosTheta / incidentCosTheta in tmm.js. */
114
119
 
115
120
  static inline cx snellCosTheta(cx n0, cx sinTheta0, cx nj) {
116
- cx sinThetaJ = cdiv(cmul(n0, sinTheta0), nj);
121
+ cx sinThetaJ = cdiv(cmul(cmk(n0.re, 0.0), sinTheta0), nj);
117
122
  return csqrt_(csub(cmk(1.0, 0.0), cmul(sinThetaJ, sinThetaJ)));
118
123
  }
119
124
 
125
+ /* cosθ0 of the incident medium: the plain cosine for a transparent medium,
126
+ * otherwise from the same real invariant as the layers. */
127
+ static inline cx incidentCosTheta(cx n0, cx sinTheta0) {
128
+ if (n0.im == 0.0) return csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
129
+ return snellCosTheta(n0, sinTheta0, n0);
130
+ }
131
+
120
132
  /* ── Layer characteristic matrix (pol: 0 = s, 1 = p) ─────────────────────── */
121
133
 
122
134
  static inline mat2 layerMatrix(cx nj, double dj_nm, double lambda_nm, cx cosTheta_j, int pol) {
@@ -149,7 +161,7 @@ static void tmm_core(double lambda_nm, double theta_deg, int pol,
149
161
  cx n0, cx ns, const double *layers, int N,
150
162
  double *outR, double *outT, double *outA) {
151
163
  cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
152
- cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
164
+ cx cosTheta0 = incidentCosTheta(n0, sinTheta0);
153
165
 
154
166
  cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
155
167
 
@@ -245,6 +257,336 @@ void tmm_spectrum(const double *lambdas, int nLam,
245
257
  if (layers) free(layers);
246
258
  }
247
259
 
260
+ /* ── Shared tails for the growing-stack kernels ──────────────────────────────
261
+ * Forward tail: reproduces the [B,C] → r,t → R,T block of tmm_core on an
262
+ * already-built characteristic matrix. A is not formed here; the incoherent
263
+ * slab combination that consumes these results computes it after the sum, and
264
+ * a semi-infinite caller forms 1−R−T itself.
265
+ *
266
+ * Reverse tail: reflectance of the same stack seen from the substrate side.
267
+ * The characteristic matrix of the reversed stack is the anti-transpose of the
268
+ * forward one (every layer matrix is invariant under anti-transposition), so
269
+ * the reverse pass costs no second product, and the common rescale factor
270
+ * cancels in the amplitude ratio. */
271
+
272
+ static inline void growingTailFwd(mat2 M, cx eta0, cx etaS, double logScale,
273
+ double *outR, double *outT) {
274
+ cx B = cadd(M.a, cmul(M.b, etaS));
275
+ cx C = cadd(M.c, cmul(M.d, etaS));
276
+ cx eta0B = cmul(eta0, B);
277
+ cx r = cdiv(csub(eta0B, C), cadd(eta0B, C));
278
+ cx t = cdiv(cmul(cmk(2.0, 0.0), eta0), cadd(eta0B, C));
279
+ double R = cabs2(r);
280
+ double T = etaS.re / eta0.re * cabs2(t) * exp(-2.0 * logScale);
281
+ if (T < 0.0) T = 0.0;
282
+ *outR = R; *outT = T;
283
+ }
284
+
285
+ static inline double growingTailRev(mat2 M, cx eta0, cx etaS) {
286
+ cx B = cadd(M.d, cmul(M.b, eta0));
287
+ cx C = cadd(M.c, cmul(M.a, eta0));
288
+ cx etaSB = cmul(etaS, B);
289
+ cx r = cdiv(csub(etaSB, C), cadd(etaSB, C));
290
+ return cabs2(r);
291
+ }
292
+
293
+ /* ── Exported: monitor curve of one growing layer ────────────────────────────
294
+ * The signal of one layer as it grows on a completed stack, at one wavelength,
295
+ * both polarizations: the incremental control algorithm of Tikhonravov &
296
+ * Trubetskov, Appl. Opt. 44, 6877 (2005). The completed stack's characteristic
297
+ * matrix is built once; each sample then costs one layer matrix, one 2×2
298
+ * multiply and the tails, so a sweep is linear in samples rather than in
299
+ * samples × stack depth.
300
+ *
301
+ * The growing layer faces the incident medium, so its matrix multiplies the
302
+ * completed product from the LEFT, exactly as createMonitorTmmEvaluator in the
303
+ * JS reference.
304
+ *
305
+ * base : NB triples [n_re, n_im, d_nm], the completed stack outermost first
306
+ * (zero-thickness entries skipped, as everywhere)
307
+ * ng : growing layer ñ at this wavelength
308
+ * dArr : nD sample thicknesses of the growing layer (nm); d ≤ 0 evaluates
309
+ * the bare completed stack
310
+ * Outputs (each nD):
311
+ * outRs/outTs, outRp/outTp : forward R and T of the coated surface
312
+ * outRrs/outRrp : its reflectance from the substrate side, for
313
+ * callers that model the witness as an incoherent
314
+ * slab with a bare back face */
315
+
316
+ TMM_EXPORT("tmm_monitor_curve")
317
+ void tmm_monitor_curve(double lambda_nm, double theta_deg,
318
+ double n0_re, double n0_im, double ns_re, double ns_im,
319
+ const double *base, int NB,
320
+ double ng_re, double ng_im,
321
+ const double *dArr, int nD,
322
+ double *outRs, double *outTs,
323
+ double *outRp, double *outTp,
324
+ double *outRrs, double *outRrp) {
325
+ cx n0 = cmk(n0_re, n0_im);
326
+ cx ns = cmk(ns_re, ns_im);
327
+ cx ng = cmk(ng_re, ng_im);
328
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
329
+ cx cosTheta0 = incidentCosTheta(n0, sinTheta0);
330
+
331
+ for (int pol = 0; pol < 2; pol++) {
332
+ cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
333
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
334
+ cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
335
+
336
+ mat2 Mb;
337
+ Mb.a = cmk(1.0, 0.0); Mb.b = cmk(0.0, 0.0);
338
+ Mb.c = cmk(0.0, 0.0); Mb.d = cmk(1.0, 0.0);
339
+ double logScaleB = 0.0;
340
+ for (int i = 0; i < NB; i++) {
341
+ cx n = cmk(base[3 * i + 0], base[3 * i + 1]);
342
+ double d = base[3 * i + 2];
343
+ if (d <= 0.0) continue;
344
+ cx cosThetaJ = snellCosTheta(n0, sinTheta0, n);
345
+ Mb = matmul(Mb, layerMatrix(n, d, lambda_nm, cosThetaJ, pol));
346
+ logScaleB += rescaleMatrix(&Mb);
347
+ }
348
+ cx cosThetaG = snellCosTheta(n0, sinTheta0, ng);
349
+
350
+ double *oR = pol ? outRp : outRs;
351
+ double *oT = pol ? outTp : outTs;
352
+ double *oRr = pol ? outRrp : outRrs;
353
+
354
+ for (int k = 0; k < nD; k++) {
355
+ mat2 M = Mb;
356
+ double logScale = logScaleB;
357
+ double d = dArr[k];
358
+ if (d > 0.0) {
359
+ M = matmul(layerMatrix(ng, d, lambda_nm, cosThetaG, pol), Mb);
360
+ logScale += rescaleMatrix(&M);
361
+ }
362
+ growingTailFwd(M, eta0, etaS, logScale, &oR[k], &oT[k]);
363
+ oRr[k] = growingTailRev(M, eta0, etaS);
364
+ }
365
+ }
366
+ }
367
+
368
+ /* ── Exported: spectra of a growing stack, one per deposited layer ───────────
369
+ * The front-surface passes of a deposition run in a single call: layers arrive
370
+ * in DEPOSITION order (first deposited, substrate-adjacent, first), each is
371
+ * folded into the running product from the left, since the newest layer faces
372
+ * the incident medium, and the spectrum after every fold is written out. The
373
+ * same incremental structure as tmm_monitor_curve over a wavelength grid, so
374
+ * all N step spectra together cost what the final one costs alone.
375
+ *
376
+ * Memory layout mirrors tmm_spectrum:
377
+ * matNK : N × nLam × 2, [layer][λ][re, im], deposition order
378
+ * thick : N thicknesses (nm); a step of zero thickness repeats the previous
379
+ * spectrum, matching the d ≤ 0 skip of the reference loop
380
+ * Outputs, each N × nLam, step-major ([step][λ]):
381
+ * outRs/outTs, outRp/outTp : forward R and T after each step
382
+ * outRrs/outRrp : reflectance from the substrate side after each
383
+ * step, for the incoherent slab combination */
384
+
385
+ /* Returns 1 on success; 0, with the outputs untouched, when the working
386
+ * state could not be allocated. */
387
+ TMM_EXPORT("tmm_deposition_spectra")
388
+ int tmm_deposition_spectra(const double *lambdas, int nLam,
389
+ const double *n0arr, const double *nsarr,
390
+ const double *matNK, const double *thick, int N,
391
+ double theta_deg,
392
+ double *outRs, double *outTs,
393
+ double *outRp, double *outTp,
394
+ double *outRrs, double *outRrp) {
395
+ /* Running product and admittances per (λ, pol); pol-major blocks. */
396
+ size_t cells = (size_t)nLam * 2;
397
+ mat2 *M = (mat2 *)malloc(sizeof(mat2) * cells);
398
+ double *logScale = (double *)malloc(sizeof(double) * cells);
399
+ cx *eta0v = (cx *)malloc(sizeof(cx) * cells);
400
+ cx *etaSv = (cx *)malloc(sizeof(cx) * cells);
401
+ if (!M || !logScale || !eta0v || !etaSv) {
402
+ free(M); free(logScale); free(eta0v); free(etaSv);
403
+ return 0;
404
+ }
405
+
406
+ cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
407
+
408
+ for (int li = 0; li < nLam; li++) {
409
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
410
+ cx ns = cmk(nsarr[2 * li + 0], nsarr[2 * li + 1]);
411
+ cx cosTheta0 = incidentCosTheta(n0, sinTheta0);
412
+ for (int pol = 0; pol < 2; pol++) {
413
+ size_t at = (size_t)pol * nLam + li;
414
+ eta0v[at] = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
415
+ cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
416
+ etaSv[at] = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
417
+ M[at].a = cmk(1.0, 0.0); M[at].b = cmk(0.0, 0.0);
418
+ M[at].c = cmk(0.0, 0.0); M[at].d = cmk(1.0, 0.0);
419
+ logScale[at] = 0.0;
420
+ }
421
+ }
422
+
423
+ for (int k = 0; k < N; k++) {
424
+ double d = thick[k];
425
+ for (int li = 0; li < nLam; li++) {
426
+ double lam = lambdas[li];
427
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
428
+ cx n = cmk(matNK[((size_t)k * nLam + li) * 2 + 0],
429
+ matNK[((size_t)k * nLam + li) * 2 + 1]);
430
+ for (int pol = 0; pol < 2; pol++) {
431
+ size_t at = (size_t)pol * nLam + li;
432
+ if (d > 0.0) {
433
+ cx cosThetaJ = snellCosTheta(n0, sinTheta0, n);
434
+ M[at] = matmul(layerMatrix(n, d, lam, cosThetaJ, pol), M[at]);
435
+ logScale[at] += rescaleMatrix(&M[at]);
436
+ }
437
+ size_t out = (size_t)k * nLam + li;
438
+ double *oR = pol ? outRp : outRs;
439
+ double *oT = pol ? outTp : outTs;
440
+ double *oRr = pol ? outRrp : outRrs;
441
+ growingTailFwd(M[at], eta0v[at], etaSv[at], logScale[at],
442
+ &oR[out], &oT[out]);
443
+ oRr[out] = growingTailRev(M[at], eta0v[at], etaSv[at]);
444
+ }
445
+ }
446
+ }
447
+
448
+ free(M); free(logScale); free(eta0v); free(etaSv);
449
+ return 1;
450
+ }
451
+
452
+ /* ── Exported: persistent growing-layer evaluator over a wavelength grid ─────
453
+ * The stateful counterpart of tmm_monitor_curve, batched the other way: one
454
+ * thickness of the growing layer per call, the WHOLE wavelength grid at once.
455
+ * This is the shape a broadband monitor scan needs, where every scan reads a
456
+ * full spectrum of the growing stack and the completed layers beneath do not
457
+ * change until the layer is cut.
458
+ *
459
+ * Lifecycle: create() folds the completed stack's characteristic matrices for
460
+ * every (λ, pol) once and keeps them; set_top() declares the growing layer's
461
+ * ñ(λ); sample() then answers one thickness across the grid, costing one layer
462
+ * matrix, one 2×2 multiply and the tails per (λ, pol). free() releases the
463
+ * state. Handles are opaque pointers; the caller owns their lifetime.
464
+ *
465
+ * Layout matches tmm_deposition_spectra: matNK is NB × nLam × 2 layer-major
466
+ * with the completed stack OUTERMOST FIRST (the fold is M = M · M_k, exactly
467
+ * the JS reference createMonitorTmmEvaluator); zero-thickness entries are
468
+ * skipped. Outputs of sample(), each nLam: forward R and T plus the
469
+ * substrate-side reflectance, for the incoherent slab combination. */
470
+
471
+ typedef struct {
472
+ int nLam;
473
+ cx sinTheta0;
474
+ int topSet;
475
+ double *lam; /* nLam */
476
+ cx *n0; /* nLam */
477
+ cx *ng; /* nLam, set_top */
478
+ cx *cosThetaG; /* nLam, set_top (polarization-independent) */
479
+ cx *eta0; /* 2·nLam, pol-major */
480
+ cx *etaS; /* 2·nLam */
481
+ mat2 *Mb; /* 2·nLam completed-stack product */
482
+ double *logScaleB; /* 2·nLam */
483
+ } growing_eval;
484
+
485
+ TMM_EXPORT("tmm_growing_eval_free")
486
+ void tmm_growing_eval_free(growing_eval *h) {
487
+ if (!h) return;
488
+ free(h->lam); free(h->n0); free(h->ng); free(h->cosThetaG);
489
+ free(h->eta0); free(h->etaS); free(h->Mb); free(h->logScaleB);
490
+ free(h);
491
+ }
492
+
493
+ TMM_EXPORT("tmm_growing_eval_create")
494
+ growing_eval *tmm_growing_eval_create(const double *lambdas, int nLam,
495
+ double theta_deg,
496
+ const double *n0arr, const double *nsarr,
497
+ const double *matNK, const double *thick,
498
+ int NB) {
499
+ growing_eval *h = (growing_eval *)malloc(sizeof(growing_eval));
500
+ if (!h) return 0;
501
+ h->nLam = nLam;
502
+ h->topSet = 0;
503
+ h->lam = (double *)malloc(sizeof(double) * nLam);
504
+ h->n0 = (cx *)malloc(sizeof(cx) * nLam);
505
+ h->ng = (cx *)malloc(sizeof(cx) * nLam);
506
+ h->cosThetaG = (cx *)malloc(sizeof(cx) * nLam);
507
+ h->eta0 = (cx *)malloc(sizeof(cx) * 2 * nLam);
508
+ h->etaS = (cx *)malloc(sizeof(cx) * 2 * nLam);
509
+ h->Mb = (mat2 *)malloc(sizeof(mat2) * 2 * nLam);
510
+ h->logScaleB = (double *)malloc(sizeof(double) * 2 * nLam);
511
+ if (!h->lam || !h->n0 || !h->ng || !h->cosThetaG
512
+ || !h->eta0 || !h->etaS || !h->Mb || !h->logScaleB) {
513
+ tmm_growing_eval_free(h);
514
+ return 0;
515
+ }
516
+
517
+ h->sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
518
+
519
+ for (int li = 0; li < nLam; li++) {
520
+ h->lam[li] = lambdas[li];
521
+ cx n0 = cmk(n0arr[2 * li + 0], n0arr[2 * li + 1]);
522
+ cx ns = cmk(nsarr[2 * li + 0], nsarr[2 * li + 1]);
523
+ h->n0[li] = n0;
524
+ cx cosTheta0 = incidentCosTheta(n0, h->sinTheta0);
525
+ for (int pol = 0; pol < 2; pol++) {
526
+ size_t at = (size_t)pol * nLam + li;
527
+ h->eta0[at] = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
528
+ cx cosThetaS = snellCosTheta(n0, h->sinTheta0, ns);
529
+ h->etaS[at] = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
530
+ mat2 M;
531
+ M.a = cmk(1.0, 0.0); M.b = cmk(0.0, 0.0);
532
+ M.c = cmk(0.0, 0.0); M.d = cmk(1.0, 0.0);
533
+ double logScale = 0.0;
534
+ for (int k = 0; k < NB; k++) {
535
+ double d = thick[k];
536
+ if (d <= 0.0) continue;
537
+ cx n = cmk(matNK[((size_t)k * nLam + li) * 2 + 0],
538
+ matNK[((size_t)k * nLam + li) * 2 + 1]);
539
+ cx cosThetaJ = snellCosTheta(n0, h->sinTheta0, n);
540
+ M = matmul(M, layerMatrix(n, d, lambdas[li], cosThetaJ, pol));
541
+ logScale += rescaleMatrix(&M);
542
+ }
543
+ h->Mb[at] = M;
544
+ h->logScaleB[at] = logScale;
545
+ }
546
+ }
547
+ return h;
548
+ }
549
+
550
+ TMM_EXPORT("tmm_growing_eval_set_top")
551
+ void tmm_growing_eval_set_top(growing_eval *h, const double *ngNK) {
552
+ if (!h) return;
553
+ for (int li = 0; li < h->nLam; li++) {
554
+ cx ng = cmk(ngNK[2 * li + 0], ngNK[2 * li + 1]);
555
+ h->ng[li] = ng;
556
+ h->cosThetaG[li] = snellCosTheta(h->n0[li], h->sinTheta0, ng);
557
+ }
558
+ h->topSet = 1;
559
+ }
560
+
561
+ /* Returns 1 on success; 0, with the outputs untouched, for a null handle or
562
+ * a d > 0 sample before set_top declared the growing layer. The status is
563
+ * what keeps an unwritten buffer from being read back as data. */
564
+ TMM_EXPORT("tmm_growing_eval_sample")
565
+ int tmm_growing_eval_sample(growing_eval *h, double d,
566
+ double *outRs, double *outTs,
567
+ double *outRp, double *outTp,
568
+ double *outRrs, double *outRrp) {
569
+ if (!h || (d > 0.0 && !h->topSet)) return 0;
570
+ for (int li = 0; li < h->nLam; li++) {
571
+ for (int pol = 0; pol < 2; pol++) {
572
+ size_t at = (size_t)pol * h->nLam + li;
573
+ mat2 M = h->Mb[at];
574
+ double logScale = h->logScaleB[at];
575
+ if (d > 0.0) {
576
+ M = matmul(layerMatrix(h->ng[li], d, h->lam[li], h->cosThetaG[li], pol),
577
+ h->Mb[at]);
578
+ logScale += rescaleMatrix(&M);
579
+ }
580
+ double *oR = pol ? outRp : outRs;
581
+ double *oT = pol ? outTp : outTs;
582
+ double *oRr = pol ? outRrp : outRrs;
583
+ growingTailFwd(M, h->eta0[at], h->etaS[at], logScale, &oR[li], &oT[li]);
584
+ oRr[li] = growingTailRev(M, h->eta0[at], h->etaS[at]);
585
+ }
586
+ }
587
+ return 1;
588
+ }
589
+
248
590
  /* ── Exported: analytic thickness Jacobian for one (λ, θ, pol) ────────────────
249
591
  * Faithful port of tmmThicknessJacobian() in thinFilmMath.js. Returns the exact
250
592
  * analytic dR/dd_k, dT/dd_k, dA/dd_k for every layer at one sample : the DLS
@@ -263,7 +605,7 @@ void tmm_jacobian(double lambda_nm, double theta_deg, int pol,
263
605
  cx ns = cmk(ns_re, ns_im);
264
606
 
265
607
  cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
266
- cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
608
+ cx cosTheta0 = incidentCosTheta(n0, sinTheta0);
267
609
  cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
268
610
  cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
269
611
  cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
@@ -366,7 +708,7 @@ void tmm_needle_scan(double lambda_nm, double theta_deg, int pol,
366
708
  double *base, double *gaps, double *intra) {
367
709
  cx n0 = cmk(n0_re, n0_im), ns = cmk(ns_re, ns_im);
368
710
  cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
369
- cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
711
+ cx cosTheta0 = incidentCosTheta(n0, sinTheta0);
370
712
  cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
371
713
  cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
372
714
  cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
@@ -475,7 +817,7 @@ void tmm_hessian(double lambda_nm, double theta_deg, int pol,
475
817
  cx ns = cmk(ns_re, ns_im);
476
818
 
477
819
  cx sinTheta0 = cmk(sin(theta_deg * PI / 180.0), 0.0);
478
- cx cosTheta0 = csqrt_(csub(cmk(1.0, 0.0), cmul(sinTheta0, sinTheta0)));
820
+ cx cosTheta0 = incidentCosTheta(n0, sinTheta0);
479
821
  cx eta0 = (pol == 0) ? cmul(n0, cosTheta0) : cdiv(n0, cosTheta0);
480
822
  cx cosThetaS = snellCosTheta(n0, sinTheta0, ns);
481
823
  cx etaS = (pol == 0) ? cmul(ns, cosThetaS) : cdiv(ns, cosThetaS);
@@ -753,10 +1095,32 @@ static double jmatmag(jmat2 M) {
753
1095
  return magnitude;
754
1096
  }
755
1097
 
1098
+ /* The real part of a jet, order by order. */
1099
+ static inline jet jreal(jet a) {
1100
+ jet o;
1101
+ for (int i = 0; i < JET_N; i++) o.c[i] = cmk(a.c[i].re, 0.0);
1102
+ return o;
1103
+ }
1104
+ static inline int jhas_imag(jet a) {
1105
+ for (int i = 0; i < JET_N; i++) if (a.c[i].im != 0.0) return 1;
1106
+ return 0;
1107
+ }
1108
+
1109
+ /* Same real invariant Re(n0) sinθ0 as snellCosTheta, in jet arithmetic. */
756
1110
  static inline jet jsnell_cos(jet n0, jet sin0, jet nj) {
757
- jet s = jdiv(jmul(n0, sin0), nj);
1111
+ jet s = jdiv(jmul(jreal(n0), sin0), nj);
758
1112
  return jsqrt_j(jsub(jconst(1.0, 0.0), jmul(s, s)));
759
1113
  }
1114
+ /* cosθ0 of the incident medium: from the same real invariant as the layers
1115
+ * when the medium absorbs at any order, otherwise the plain cosine, taken
1116
+ * from the sine jet when the caller supplied one. Mirrors incidentCosine in
1117
+ * phase.js. */
1118
+ static inline jet jincident_cos(jet n0, jet incidentSine, int fromSineJet, double theta_deg) {
1119
+ if (jhas_imag(n0)) return jsnell_cos(n0, incidentSine, n0);
1120
+ return fromSineJet
1121
+ ? jsqrt_j(jsub(jconst(1.0, 0.0), jmul(incidentSine, incidentSine)))
1122
+ : jconst(cos(theta_deg * PI / 180.0), 0.0);
1123
+ }
760
1124
  static inline jet jadmittance(jet n, jet cosv, int pol) {
761
1125
  return (pol == 0) ? jmul(n, cosv) : jdiv(n, cosv);
762
1126
  }
@@ -877,9 +1241,7 @@ static void jphase_core(double lambda, double omega, double theta_deg, int pol,
877
1241
  const jet *sinJet, double *out10) {
878
1242
  jet wavelength = jwavelength(lambda, omega);
879
1243
  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);
1244
+ jet incidentCosine = jincident_cos(n0, incidentSine, sinJet != NULL, theta_deg);
883
1245
  jet incidentEta = jadmittance(n0, incidentCosine, pol);
884
1246
  jet substrateCosine = jsnell_cos(n0, incidentSine, ns);
885
1247
  jet substrateEta = jadmittance(ns, substrateCosine, pol);
@@ -983,9 +1345,7 @@ void tmm_phase_jacobian(double lambda, double omega, double theta_deg, int pol,
983
1345
 
984
1346
  jet wavelength = jwavelength(lambda, omega);
985
1347
  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);
1348
+ jet incidentCosine = jincident_cos(n0, incidentSine, sinePtr != NULL, theta_deg);
989
1349
  jet incidentEta = jadmittance(n0, incidentCosine, pol);
990
1350
  jet substrateCosine = jsnell_cos(n0, incidentSine, ns);
991
1351
  jet substrateEta = jadmittance(ns, substrateCosine, pol);
Binary file