tmmcore 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmmcore",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
23
+ "test": "node tests/equivalence.mjs && node tests/growing_equivalence.mjs && node tests/growing_eval_equivalence.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/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
@@ -245,6 +245,336 @@ void tmm_spectrum(const double *lambdas, int nLam,
245
245
  if (layers) free(layers);
246
246
  }
247
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
+
248
578
  /* ── Exported: analytic thickness Jacobian for one (λ, θ, pol) ────────────────
249
579
  * Faithful port of tmmThicknessJacobian() in thinFilmMath.js. Returns the exact
250
580
  * analytic dR/dd_k, dT/dd_k, dA/dd_k for every layer at one sample : the DLS
Binary file