tmmcore 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tmmWasm.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * tmmWasm.js loader and ergonomic wrappers for the WebAssembly TMM kernel.
2
+ * tmmWasm.js : loader and ergonomic wrappers for the WebAssembly TMM kernel.
3
3
  *
4
4
  * The kernel (`tmm_kernel.c`, built to `tmm_kernel.wasm`) is a line-by-line port
5
- * of the JavaScript TMM in `tmm.js`. This module instantiates it in a browser
6
- * main thread, a Web Worker, or Node and exposes wrappers whose signatures
5
+ * of the JavaScript TMM in `tmm.js`. This module instantiates it : in a browser
6
+ * main thread, a Web Worker, or Node : and exposes wrappers whose signatures
7
7
  * mirror the JS functions.
8
8
  *
9
9
  * Acceleration is opt-in and falls back to JavaScript: if the `.wasm` is
@@ -11,16 +11,44 @@
11
11
  * return `null` and callers use the JS path. Results are identical either way
12
12
  * to float64 round-off.
13
13
  *
14
- * Instances are not shared across threads there is no shared memory, so each
14
+ * Instances are not shared across threads : there is no shared memory, so each
15
15
  * context instantiates its own from the same bytes. Use `instantiateTmmWasm()`
16
16
  * where you already hold the bytes (a worker receives them in its init message)
17
17
  * and `initTmmWasmFromUrl()` where the artifact is fetchable.
18
18
  */
19
19
 
20
+ import { omegaFromLambdaNm } from './phase.js';
21
+
20
22
  let _instance = null; // TmmWasmInstance | null
21
23
  let _enabled = false; // feature flag (default OFF)
22
24
  let _initPromise = null; // de-dupe concurrent init
23
25
 
26
+ // ── Jet marshalling ──────────────────────────────────────────────────────────
27
+ // A jet crosses the boundary as 8 doubles, [re, im] per order.
28
+
29
+ function writeJet(buf, offset, jet) {
30
+ for (let i = 0; i < 4; i++) {
31
+ buf[offset + 2 * i] = jet[i][0];
32
+ buf[offset + 2 * i + 1] = jet[i][1];
33
+ }
34
+ }
35
+
36
+ // The kernel writes NaN where the coefficient is exactly zero and the phase is
37
+ // undefined; the JS reference returns null there, so agree with it.
38
+ function readPhase(buf, offset) {
39
+ const magnitudeSquared = buf[offset + 4];
40
+ if (Number.isNaN(magnitudeSquared)) return null;
41
+ const phaseRad = buf[offset];
42
+ return {
43
+ phaseRad,
44
+ phaseDeg: phaseRad * 180 / Math.PI,
45
+ gd: buf[offset + 1],
46
+ gdd: buf[offset + 2],
47
+ tod: buf[offset + 3],
48
+ magnitudeSquared,
49
+ };
50
+ }
51
+
24
52
  // Permissive imports: a STANDALONE_WASM build of pure-math C usually needs no
25
53
  // imports, but ALLOW_MEMORY_GROWTH may emit `emscripten_notify_memory_growth`,
26
54
  // and some toolchains emit WASI stubs. Cover them so instantiation never throws.
@@ -31,6 +59,15 @@ function wasmImports() {
31
59
  };
32
60
  }
33
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
+
34
71
  export class TmmWasmInstance {
35
72
  constructor(instance) {
36
73
  const ex = instance.exports;
@@ -49,6 +86,21 @@ export class TmmWasmInstance {
49
86
  // Optional (added later for SQP/Newton accel): a .wasm built before the
50
87
  // Hessian kernel existed simply lacks it → callers fall back to JS.
51
88
  this._tmm_hessian = ex.tmm_hessian || ex._tmm_hessian || null;
89
+ // Optional, same reason: the phase-dispersion kernel arrived after the
90
+ // spectral one, so an older artifact lacks these three.
91
+ this._tmm_phase_one = ex.tmm_phase_one || ex._tmm_phase_one || null;
92
+ this._tmm_phase_spectrum = ex.tmm_phase_spectrum || ex._tmm_phase_spectrum || null;
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;
52
104
  const missingExports = !this.malloc || !this.free || !this._tmm_one ||
53
105
  !this._tmm_spectrum || !this._tmm_jacobian || !this._tmm_needle_scan;
54
106
  if (missingExports) {
@@ -63,7 +115,7 @@ export class TmmWasmInstance {
63
115
  if (!ptr) throw new Error('tmmWasm: malloc failed');
64
116
  return ptr;
65
117
  }
66
- // Fresh view memory.buffer is detached after any growth, so re-create
118
+ // Fresh view : memory.buffer is detached after any growth, so re-create
67
119
  // views AFTER all mallocs for a call are done.
68
120
  _view(ptr, nDoubles) {
69
121
  return new Float64Array(this.memory.buffer, ptr, nDoubles);
@@ -84,7 +136,7 @@ export class TmmWasmInstance {
84
136
  }
85
137
 
86
138
  /**
87
- * Single (λ, θ, pol) mirrors tmm() in thinFilmMath.js.
139
+ * Single (λ, θ, pol) : mirrors tmm() in thinFilmMath.js.
88
140
  * @returns {{R:number,T:number,A:number}}
89
141
  */
90
142
  tmmOne(lambda_nm, theta_deg, polCode /* 0=s,1=p */, n0, ns, layers) {
@@ -104,7 +156,7 @@ export class TmmWasmInstance {
104
156
  }
105
157
 
106
158
  /**
107
- * Batched spectrum over a λ grid for BOTH polarizations the boundary-
159
+ * Batched spectrum over a λ grid for BOTH polarizations : the boundary-
108
160
  * amortizing path behind evaluateSpectrum().
109
161
  * @param {number[]} lambdas
110
162
  * @param {[number,number][]} n0List incident ñ per λ
@@ -159,8 +211,236 @@ export class TmmWasmInstance {
159
211
  return res;
160
212
  }
161
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
+
162
225
  /**
163
- * Analytic thickness Jacobian for one (λ, θ, pol) mirrors
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
+
442
+ /**
443
+ * Analytic thickness Jacobian for one (λ, θ, pol) : mirrors
164
444
  * tmmThicknessJacobian(). layers used AS-IS (index parity).
165
445
  * @returns {{R,T,A, dRdd:Float64Array, dTdd, dAdd, N}}
166
446
  */
@@ -182,7 +462,7 @@ export class TmmWasmInstance {
182
462
  n0[0], n0[1], ns[0], ns[1], P(oLay), N, P(oDR), P(oDT), P(oDA), P(oBase));
183
463
  // Re-create the view AFTER the kernel call (like tmmSpectrum): under
184
464
  // ALLOW_MEMORY_GROWTH the kernel may grow wasm memory, which detaches the
185
- // ArrayBuffer `buf` was created over reading the stale `buf` then yields
465
+ // ArrayBuffer `buf` was created over : reading the stale `buf` then yields
186
466
  // garbage / throws. `out` is a fresh view over the current buffer.
187
467
  const out = this._view(ptr, need);
188
468
  return {
@@ -197,7 +477,7 @@ export class TmmWasmInstance {
197
477
  hasHessian() { return !!this._tmm_hessian; }
198
478
 
199
479
  /**
200
- * Analytic thickness Hessian for one (λ, θ, pol) mirrors
480
+ * Analytic thickness Hessian for one (λ, θ, pol) : mirrors
201
481
  * tmmThicknessHessian(). Returns first AND second derivatives; the N×N
202
482
  * second-derivative blocks are reshaped into nested arrays (one Float64Array
203
483
  * row per layer, FULL symmetric) so the shape matches the JS oracle exactly.
@@ -238,8 +518,169 @@ export class TmmWasmInstance {
238
518
  };
239
519
  }
240
520
 
521
+ /** True if the loaded module carries the phase-dispersion kernel. */
522
+ hasPhase() { return !!this._tmm_phase_one; }
523
+
524
+ /**
525
+ * Phase, group delay, GDD and TOD at one wavelength : mirrors
526
+ * tmmPhaseDispersion() in phase.js.
527
+ *
528
+ * @param {number[][]} n0Jet incident-medium index jet, 4 × [re, im]
529
+ * @param {number[][]} nsJet substrate index jet
530
+ * @param {{nJet:number[][], d:number}[]} layers
531
+ * @param {{omega?:number, sinTheta0Jet?:number[][]}} [options]
532
+ * @returns {{r: object|null, t: object|null}}
533
+ */
534
+ tmmPhaseOne(lambda_nm, theta_deg, polCode, n0Jet, nsJet, layers, options = {}) {
535
+ const N = layers.length;
536
+ const omega = options.omega ?? omegaFromLambdaNm(lambda_nm);
537
+ const sinJet = options.sinTheta0Jet || null;
538
+ // arena: layerJets[8N] | thick[N] | n0[8] | ns[8] | sin[8] | out[10]
539
+ const oLay = 0, oThick = 8 * N, oN0 = oThick + N, oNs = oN0 + 8,
540
+ oSin = oNs + 8, oOut = oSin + 8;
541
+ const need = oOut + 10;
542
+ const ptr = this._scratch(need);
543
+ const buf = this._view(ptr, need);
544
+ for (let i = 0; i < N; i++) {
545
+ writeJet(buf, oLay + 8 * i, layers[i].nJet);
546
+ buf[oThick + i] = layers[i].d;
547
+ }
548
+ writeJet(buf, oN0, n0Jet);
549
+ writeJet(buf, oNs, nsJet);
550
+ if (sinJet) writeJet(buf, oSin, sinJet);
551
+ const P = (off) => ptr + off * 8;
552
+ this._tmm_phase_one(lambda_nm, omega, theta_deg, polCode | 0,
553
+ P(oN0), P(oNs), P(oLay), P(oThick), N, sinJet ? P(oSin) : 0, P(oOut));
554
+ const out = this._view(ptr, need);
555
+ return { r: readPhase(out, oOut), t: readPhase(out, oOut + 5) };
556
+ }
557
+
558
+ /**
559
+ * Batched phase dispersion over a λ grid : the boundary-amortizing path.
560
+ *
561
+ * Unlike `tmmSpectrum` this takes one polarization, because the kernel is an
562
+ * order of magnitude dearer per sample and callers at normal incidence would
563
+ * otherwise pay twice for the same numbers.
564
+ *
565
+ * @param {number[]} lambdas
566
+ * @param {number[][][]} n0Jets index jet per λ
567
+ * @param {number[][][]} nsJets index jet per λ
568
+ * @param {number[][][][]} layerJets [layer][λ] = index jet
569
+ * @param {number[]} thick layer thicknesses (nm), length N
570
+ * @param {{omegas?:number[], sinJets?:number[][][]}} [options]
571
+ * @returns {{r: object, t: object}} each `{phaseRad, gd, gdd, tod,
572
+ * magnitudeSquared}` of Float64Array(nLam). Failed samples hold NaN.
573
+ */
574
+ tmmPhaseSpectrum(lambdas, n0Jets, nsJets, layerJets, thick, theta_deg, polCode, options = {}) {
575
+ const nLam = lambdas.length;
576
+ const N = thick.length;
577
+ const omegas = options.omegas
578
+ || lambdas.map(lambda => omegaFromLambdaNm(lambda));
579
+ const sinJets = options.sinJets || null;
580
+
581
+ const lamPtr = this._alloc(nLam);
582
+ const omPtr = this._alloc(nLam);
583
+ const n0Ptr = this._alloc(8 * nLam);
584
+ const nsPtr = this._alloc(8 * nLam);
585
+ const matPtr = this._alloc(Math.max(1, 8 * N * nLam));
586
+ const thPtr = this._alloc(Math.max(1, N));
587
+ const sinPtr = sinJets ? this._alloc(8 * nLam) : 0;
588
+ const outPtr = this._alloc(10 * nLam);
589
+
590
+ // Views created after all mallocs (the buffer may have grown/detached).
591
+ const lam = this._view(lamPtr, nLam);
592
+ const om = this._view(omPtr, nLam);
593
+ const n0v = this._view(n0Ptr, 8 * nLam);
594
+ const nsv = this._view(nsPtr, 8 * nLam);
595
+ const matv = this._view(matPtr, Math.max(1, 8 * N * nLam));
596
+ const thv = this._view(thPtr, Math.max(1, N));
597
+ const sinv = sinJets ? this._view(sinPtr, 8 * nLam) : null;
598
+ for (let i = 0; i < nLam; i++) {
599
+ lam[i] = lambdas[i];
600
+ om[i] = omegas[i];
601
+ writeJet(n0v, 8 * i, n0Jets[i]);
602
+ writeJet(nsv, 8 * i, nsJets[i]);
603
+ if (sinv) writeJet(sinv, 8 * i, sinJets[i]);
604
+ }
605
+ for (let k = 0; k < N; k++) {
606
+ thv[k] = thick[k];
607
+ const row = layerJets[k];
608
+ const base = k * nLam * 8;
609
+ for (let i = 0; i < nLam; i++) writeJet(matv, base + 8 * i, row[i]);
610
+ }
611
+
612
+ this._tmm_phase_spectrum(lamPtr, omPtr, nLam, n0Ptr, nsPtr, matPtr, thPtr, N,
613
+ theta_deg, polCode | 0, sinPtr, outPtr);
614
+
615
+ // De-interleave into one array per quantity before freeing.
616
+ const out = this._view(outPtr, 10 * nLam);
617
+ const side = (base) => {
618
+ const q = {
619
+ phaseRad: new Float64Array(nLam), gd: new Float64Array(nLam),
620
+ gdd: new Float64Array(nLam), tod: new Float64Array(nLam),
621
+ magnitudeSquared: new Float64Array(nLam),
622
+ };
623
+ const keys = ['phaseRad', 'gd', 'gdd', 'tod', 'magnitudeSquared'];
624
+ for (let i = 0; i < nLam; i++) {
625
+ for (let j = 0; j < 5; j++) q[keys[j]][i] = out[10 * i + base + j];
626
+ }
627
+ return q;
628
+ };
629
+ const result = { r: side(0), t: side(5) };
630
+ for (const p of [lamPtr, omPtr, n0Ptr, nsPtr, matPtr, thPtr, outPtr]) this.free(p);
631
+ if (sinPtr) this.free(sinPtr);
632
+ return result;
633
+ }
634
+
635
+ /**
636
+ * Phase dispersion plus exact thickness derivatives : mirrors
637
+ * tmmPhaseThicknessJacobian(). Layers used AS-IS (index parity).
638
+ *
639
+ * @returns {{r, t}} each the phase quantities plus `dPhaseDeg`, `dGd`,
640
+ * `dGdd`, `dTod` as Float64Array(N), or `null` arrays on overflow.
641
+ */
642
+ tmmPhaseJacobian(lambda_nm, theta_deg, polCode, n0Jet, nsJet, layers, options = {}) {
643
+ const N = layers.length;
644
+ const M = Math.max(1, N);
645
+ const omega = options.omega ?? omegaFromLambdaNm(lambda_nm);
646
+ const sinJet = options.sinTheta0Jet || null;
647
+ // arena: layerJets[8N] | thick[N] | n0[8] | ns[8] | sin[8] | out[10] | deriv[8M]
648
+ const oLay = 0, oThick = 8 * N, oN0 = oThick + N, oNs = oN0 + 8,
649
+ oSin = oNs + 8, oOut = oSin + 8, oDeriv = oOut + 10;
650
+ const need = oDeriv + 8 * M;
651
+ const ptr = this._scratch(need);
652
+ const buf = this._view(ptr, need);
653
+ for (let i = 0; i < N; i++) {
654
+ writeJet(buf, oLay + 8 * i, layers[i].nJet);
655
+ buf[oThick + i] = layers[i].d;
656
+ }
657
+ writeJet(buf, oN0, n0Jet);
658
+ writeJet(buf, oNs, nsJet);
659
+ if (sinJet) writeJet(buf, oSin, sinJet);
660
+ const P = (off) => ptr + off * 8;
661
+ this._tmm_phase_jacobian(lambda_nm, omega, theta_deg, polCode | 0,
662
+ P(oN0), P(oNs), P(oLay), P(oThick), N, sinJet ? P(oSin) : 0,
663
+ P(oOut), P(oDeriv));
664
+ const out = this._view(ptr, need);
665
+ const side = (phaseBase, derivBase) => {
666
+ const base = readPhase(out, phaseBase);
667
+ if (!base) return null;
668
+ // The kernel fills the whole block with NaN when the matrix product
669
+ // overflowed and the prefix/suffix decomposition had to be abandoned.
670
+ const overflowed = N > 0 && Number.isNaN(out[oDeriv + derivBase * N]);
671
+ const take = (q) => overflowed
672
+ ? null
673
+ : out.slice(oDeriv + (derivBase + q) * N, oDeriv + (derivBase + q) * N + N);
674
+ return {
675
+ ...base,
676
+ dPhaseDeg: take(0), dGd: take(1), dGdd: take(2), dTod: take(3),
677
+ };
678
+ };
679
+ return { r: side(oOut, 0), t: side(oOut + 5, 4) };
680
+ }
681
+
241
682
  /**
242
- * Analytic needle P-function scan mirrors tmmNeedleScan() in
683
+ * Analytic needle P-function scan : mirrors tmmNeedleScan() in
243
684
  * thinFilmMath.js, reshaping the flat WASM output into the SAME nested
244
685
  * structure the synthesis scanners consume.
245
686
  * @param {{n:[number,number],d:number}[]} layers used AS-IS (index parity)
@@ -332,7 +773,7 @@ export function initTmmWasmFromUrl(url) {
332
773
  await instantiateTmmWasm(buf);
333
774
  return true;
334
775
  } catch (e) {
335
- // Not built yet / not found silent fallback to JS.
776
+ // Not built yet / not found : silent fallback to JS.
336
777
  _instance = null;
337
778
  return false;
338
779
  }
@@ -371,7 +812,7 @@ export async function initTmmWasmMainThread(bytes, enabled) {
371
812
  return true;
372
813
  }
373
814
 
374
- /** MAIN THREAD: bytes to ship to a worker only when the feature is active. */
815
+ /** MAIN THREAD: bytes to ship to a worker : only when the feature is active. */
375
816
  export function getTmmWasmBytesForWorker() {
376
817
  return (_enabled && _workerBytes) ? _workerBytes : null;
377
818
  }