tmmcore 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tmmWasm.js CHANGED
@@ -1,839 +1,931 @@
1
- /**
2
- * tmmWasm.js : loader and ergonomic wrappers for the WebAssembly TMM kernel.
3
- *
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
7
- * mirror the JS functions.
8
- *
9
- * Acceleration is opt-in and falls back to JavaScript: if the `.wasm` is
10
- * unavailable, instantiation fails, or the feature flag is off, the wrappers
11
- * return `null` and callers use the JS path. Results are identical either way
12
- * to float64 round-off.
13
- *
14
- * Instances are not shared across threads : there is no shared memory, so each
15
- * context instantiates its own from the same bytes. Use `instantiateTmmWasm()`
16
- * where you already hold the bytes (a worker receives them in its init message)
17
- * and `initTmmWasmFromUrl()` where the artifact is fetchable.
18
- */
19
-
20
- import { omegaFromLambdaNm } from './phase.js';
21
-
22
- let _instance = null; // TmmWasmInstance | null
23
- let _enabled = false; // feature flag (default OFF)
24
- let _initPromise = null; // de-dupe concurrent init
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
-
52
- // Permissive imports: a STANDALONE_WASM build of pure-math C usually needs no
53
- // imports, but ALLOW_MEMORY_GROWTH may emit `emscripten_notify_memory_growth`,
54
- // and some toolchains emit WASI stubs. Cover them so instantiation never throws.
55
- function wasmImports() {
56
- return {
57
- env: { emscripten_notify_memory_growth: () => {} },
58
- wasi_snapshot_preview1: new Proxy({}, { get: () => () => 0 }),
59
- };
60
- }
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
-
71
- export class TmmWasmInstance {
72
- constructor(instance) {
73
- const ex = instance.exports;
74
- this.exports = ex;
75
- // STANDALONE_WASM reactor modules expose an initializer that must run
76
- // before malloc (sets up the allocator + any static ctors). Call it once.
77
- if (typeof ex._initialize === 'function') ex._initialize();
78
- else if (typeof ex.__wasm_call_ctors === 'function') ex.__wasm_call_ctors();
79
- this.memory = ex.memory;
80
- this.malloc = ex.malloc || ex._malloc;
81
- this.free = ex.free || ex._free;
82
- this._tmm_one = ex.tmm_one || ex._tmm_one;
83
- this._tmm_spectrum = ex.tmm_spectrum || ex._tmm_spectrum;
84
- this._tmm_jacobian = ex.tmm_jacobian || ex._tmm_jacobian;
85
- this._tmm_needle_scan = ex.tmm_needle_scan || ex._tmm_needle_scan;
86
- // Optional (added later for SQP/Newton accel): a .wasm built before the
87
- // Hessian kernel existed simply lacks it → callers fall back to JS.
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;
104
- const missingExports = !this.malloc || !this.free || !this._tmm_one ||
105
- !this._tmm_spectrum || !this._tmm_jacobian || !this._tmm_needle_scan;
106
- if (missingExports) {
107
- throw new Error('tmmWasm: required exports missing from module');
108
- }
109
- this._scratchPtr = 0; // persistent per-call scratch arena (lazy)
110
- this._scratchN = 0;
111
- }
112
-
113
- _alloc(nDoubles) {
114
- const ptr = this.malloc(nDoubles * 8);
115
- if (!ptr) throw new Error('tmmWasm: malloc failed');
116
- return ptr;
117
- }
118
- // Fresh view : memory.buffer is detached after any growth, so re-create
119
- // views AFTER all mallocs for a call are done.
120
- _view(ptr, nDoubles) {
121
- return new Float64Array(this.memory.buffer, ptr, nDoubles);
122
- }
123
-
124
- // Persistent scratch arena for the per-call hot paths (tmmOne/tmmJacobian).
125
- // These are invoked thousands of times per optimization run; malloc/free +
126
- // typed-array churn per call would dominate the JS↔WASM boundary cost and
127
- // can make per-call WASM SLOWER than JS. Reusing one buffer (grown on demand,
128
- // never freed between calls) makes each call just write-args / read-result.
129
- _scratch(nDoubles) {
130
- if (this._scratchN < nDoubles) {
131
- if (this._scratchPtr) this.free(this._scratchPtr);
132
- this._scratchPtr = this._alloc(nDoubles);
133
- this._scratchN = nDoubles;
134
- }
135
- return this._scratchPtr;
136
- }
137
-
138
- /**
139
- * Single (λ, θ, pol) : mirrors tmm() in thinFilmMath.js.
140
- * @returns {{R:number,T:number,A:number}}
141
- */
142
- tmmOne(lambda_nm, theta_deg, polCode /* 0=s,1=p */, n0, ns, layers) {
143
- const N = layers.length;
144
- const need = 3 * N + 3; // layers [0..3N) + out [3N..3N+3)
145
- const ptr = this._scratch(need); // may grow→detach; view created AFTER
146
- const buf = this._view(ptr, need);
147
- for (let i = 0; i < N; i++) {
148
- buf[3 * i + 0] = layers[i].n[0];
149
- buf[3 * i + 1] = layers[i].n[1];
150
- buf[3 * i + 2] = layers[i].d;
151
- }
152
- const outPtr = ptr + 3 * N * 8;
153
- this._tmm_one(lambda_nm, theta_deg, polCode | 0,
154
- n0[0], n0[1], ns[0], ns[1], ptr, N, outPtr);
155
- return { R: buf[3 * N], T: buf[3 * N + 1], A: buf[3 * N + 2] };
156
- }
157
-
158
- /**
159
- * Batched spectrum over a λ grid for BOTH polarizations : the boundary-
160
- * amortizing path behind evaluateSpectrum().
161
- * @param {number[]} lambdas
162
- * @param {[number,number][]} n0List incident ñ per λ
163
- * @param {[number,number][]} nsList substrate ñ per λ
164
- * @param {[number,number][][]} layerNK [layer][λ] = ñ
165
- * @param {number[]} thick layer thicknesses (nm), length N
166
- * @param {number} theta_deg
167
- * @returns {{Rs,Ts,As,Rp,Tp,Ap}} each a Float64Array(nLam)
168
- */
169
- tmmSpectrum(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
170
- const nLam = lambdas.length;
171
- const N = thick.length;
172
-
173
- const lamPtr = this._alloc(nLam);
174
- const n0Ptr = this._alloc(2 * nLam);
175
- const nsPtr = this._alloc(2 * nLam);
176
- const mPtr = this._alloc(Math.max(1, 2 * N * nLam));
177
- const thPtr = this._alloc(Math.max(1, N));
178
- const rsPtr = this._alloc(nLam), tsPtr = this._alloc(nLam), asPtr = this._alloc(nLam);
179
- const rpPtr = this._alloc(nLam), tpPtr = this._alloc(nLam), apPtr = this._alloc(nLam);
180
-
181
- // Views created after all mallocs (buffer may have grown/detached).
182
- const lam = this._view(lamPtr, nLam);
183
- const n0v = this._view(n0Ptr, 2 * nLam);
184
- const nsv = this._view(nsPtr, 2 * nLam);
185
- const mv = this._view(mPtr, Math.max(1, 2 * N * nLam));
186
- const thv = this._view(thPtr, Math.max(1, N));
187
- for (let i = 0; i < nLam; i++) {
188
- lam[i] = lambdas[i];
189
- n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
190
- nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
191
- }
192
- for (let k = 0; k < N; k++) {
193
- thv[k] = thick[k];
194
- const row = layerNK[k];
195
- const base = k * nLam * 2;
196
- for (let i = 0; i < nLam; i++) {
197
- mv[base + 2 * i] = row[i][0];
198
- mv[base + 2 * i + 1] = row[i][1];
199
- }
200
- }
201
-
202
- this._tmm_spectrum(lamPtr, nLam, n0Ptr, nsPtr, mPtr, thPtr, N, theta_deg,
203
- rsPtr, tsPtr, asPtr, rpPtr, tpPtr, apPtr);
204
-
205
- // Copy outputs out of wasm memory before freeing.
206
- const cp = (p) => Float64Array.from(this._view(p, nLam));
207
- const res = { Rs: cp(rsPtr), Ts: cp(tsPtr), As: cp(asPtr),
208
- Rp: cp(rpPtr), Tp: cp(tpPtr), Ap: cp(apPtr) };
209
- for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr,
210
- rsPtr, tsPtr, asPtr, rpPtr, tpPtr, apPtr]) this.free(p);
211
- return res;
212
- }
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
-
442
- /**
443
- * Analytic thickness Jacobian for one (λ, θ, pol) : mirrors
444
- * tmmThicknessJacobian(). layers used AS-IS (index parity).
445
- * @returns {{R,T,A, dRdd:Float64Array, dTdd, dAdd, N}}
446
- */
447
- tmmJacobian(lambda_nm, theta_deg, polCode, n0, ns, layers) {
448
- const N = layers.length;
449
- const M = Math.max(1, N);
450
- // arena layout: layers[3N] | dRdd[M] | dTdd[M] | dAdd[M] | base[3]
451
- const oLay = 0, oDR = 3 * N, oDT = oDR + M, oDA = oDT + M, oBase = oDA + M;
452
- const need = oBase + 3;
453
- const ptr = this._scratch(need);
454
- const buf = this._view(ptr, need);
455
- for (let i = 0; i < N; i++) {
456
- buf[3 * i + 0] = layers[i].n[0];
457
- buf[3 * i + 1] = layers[i].n[1];
458
- buf[3 * i + 2] = layers[i].d;
459
- }
460
- const P = (off) => ptr + off * 8;
461
- this._tmm_jacobian(lambda_nm, theta_deg, polCode | 0,
462
- n0[0], n0[1], ns[0], ns[1], P(oLay), N, P(oDR), P(oDT), P(oDA), P(oBase));
463
- // Re-create the view AFTER the kernel call (like tmmSpectrum): under
464
- // ALLOW_MEMORY_GROWTH the kernel may grow wasm memory, which detaches the
465
- // ArrayBuffer `buf` was created over : reading the stale `buf` then yields
466
- // garbage / throws. `out` is a fresh view over the current buffer.
467
- const out = this._view(ptr, need);
468
- return {
469
- R: out[oBase], T: out[oBase + 1], A: out[oBase + 2], N,
470
- dRdd: out.slice(oDR, oDR + N),
471
- dTdd: out.slice(oDT, oDT + N),
472
- dAdd: out.slice(oDA, oDA + N),
473
- };
474
- }
475
-
476
- /** True if the loaded module carries the Hessian kernel (newer build). */
477
- hasHessian() { return !!this._tmm_hessian; }
478
-
479
- /**
480
- * Analytic thickness Hessian for one (λ, θ, pol) : mirrors
481
- * tmmThicknessHessian(). Returns first AND second derivatives; the N×N
482
- * second-derivative blocks are reshaped into nested arrays (one Float64Array
483
- * row per layer, FULL symmetric) so the shape matches the JS oracle exactly.
484
- * @returns {{R,T,A, dRdd, dTdd, dAdd, d2Rdd, d2Tdd, d2Add, N}}
485
- */
486
- tmmHessian(lambda_nm, theta_deg, polCode, n0, ns, layers) {
487
- const N = layers.length;
488
- const M = Math.max(1, N);
489
- const NN = Math.max(1, N * N);
490
- // arena: layers[3N] | dRdd[M] | dTdd[M] | dAdd[M] | d2R[NN] | d2T[NN] | d2A[NN] | base[3]
491
- const oLay = 0, oDR = 3 * N, oDT = oDR + M, oDA = oDT + M,
492
- oR2 = oDA + M, oT2 = oR2 + NN, oA2 = oT2 + NN, oBase = oA2 + NN;
493
- const need = oBase + 3;
494
- const ptr = this._scratch(need);
495
- const buf = this._view(ptr, need);
496
- for (let i = 0; i < N; i++) {
497
- buf[3 * i + 0] = layers[i].n[0];
498
- buf[3 * i + 1] = layers[i].n[1];
499
- buf[3 * i + 2] = layers[i].d;
500
- }
501
- const P = (off) => ptr + off * 8;
502
- this._tmm_hessian(lambda_nm, theta_deg, polCode | 0,
503
- n0[0], n0[1], ns[0], ns[1], P(oLay), N,
504
- P(oDR), P(oDT), P(oDA), P(oR2), P(oT2), P(oA2), P(oBase));
505
- // Fresh view after the call (memory may have grown buf detached).
506
- const out = this._view(ptr, need);
507
- const reshape = (off) => {
508
- const rows = new Array(N);
509
- for (let i = 0; i < N; i++) rows[i] = out.slice(off + i * N, off + i * N + N);
510
- return rows;
511
- };
512
- return {
513
- R: out[oBase], T: out[oBase + 1], A: out[oBase + 2], N,
514
- dRdd: out.slice(oDR, oDR + N),
515
- dTdd: out.slice(oDT, oDT + N),
516
- dAdd: out.slice(oDA, oDA + N),
517
- d2Rdd: reshape(oR2), d2Tdd: reshape(oT2), d2Add: reshape(oA2),
518
- };
519
- }
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
-
682
- /**
683
- * Analytic needle P-function scan : mirrors tmmNeedleScan() in
684
- * thinFilmMath.js, reshaping the flat WASM output into the SAME nested
685
- * structure the synthesis scanners consume.
686
- * @param {{n:[number,number],d:number}[]} layers used AS-IS (index parity)
687
- * @param {[number,number][]} candidateNs candidate ñ
688
- * @param {number[]} intraFracs intra-layer split fractions
689
- * @returns {{R,T,A,N, gaps:Array, intra:Array}}
690
- * gaps[pos][ci] = {dR,dT,dA} (pos = 0..N)
691
- * intra[k][fi] = {frac, perCand:[{dR,dT,dA}]}
692
- */
693
- tmmNeedleScan(lambda_nm, theta_deg, polCode, n0, ns, layers, candidateNs, intraFracs = []) {
694
- const N = layers.length;
695
- const nCand = candidateNs.length;
696
- const nFrac = intraFracs.length;
697
- const nGap = (N + 1) * nCand * 3;
698
- const nIntra = Math.max(1, N * nFrac * nCand * 3);
699
-
700
- const layPtr = this._alloc(Math.max(1, 3 * N));
701
- const candPtr = this._alloc(Math.max(1, 2 * nCand));
702
- const fracPtr = this._alloc(Math.max(1, nFrac));
703
- const basePtr = this._alloc(3);
704
- const gapPtr = this._alloc(Math.max(1, nGap));
705
- const intraPtr = this._alloc(nIntra);
706
-
707
- const lay = this._view(layPtr, Math.max(1, 3 * N));
708
- for (let i = 0; i < N; i++) {
709
- lay[3 * i + 0] = layers[i].n[0];
710
- lay[3 * i + 1] = layers[i].n[1];
711
- lay[3 * i + 2] = layers[i].d;
712
- }
713
- const cand = this._view(candPtr, Math.max(1, 2 * nCand));
714
- for (let c = 0; c < nCand; c++) { cand[2 * c] = candidateNs[c][0]; cand[2 * c + 1] = candidateNs[c][1]; }
715
- const frac = this._view(fracPtr, Math.max(1, nFrac));
716
- for (let i = 0; i < nFrac; i++) frac[i] = intraFracs[i];
717
-
718
- this._tmm_needle_scan(lambda_nm, theta_deg, polCode | 0,
719
- n0[0], n0[1], ns[0], ns[1], layPtr, N, candPtr, nCand, fracPtr, nFrac,
720
- basePtr, gapPtr, intraPtr);
721
-
722
- // Copy outputs out before freeing, reshaping to the JS nested layout.
723
- const base = this._view(basePtr, 3);
724
- const R = base[0], T = base[1], A = base[2];
725
- const gapV = this._view(gapPtr, Math.max(1, nGap));
726
- const gaps = new Array(N + 1);
727
- for (let pos = 0; pos <= N; pos++) {
728
- const row = new Array(nCand);
729
- for (let c = 0; c < nCand; c++) {
730
- const o = (pos * nCand + c) * 3;
731
- row[c] = { dR: gapV[o], dT: gapV[o + 1], dA: gapV[o + 2] };
732
- }
733
- gaps[pos] = row;
734
- }
735
- const intra = [];
736
- if (nFrac > 0) {
737
- const intraV = this._view(intraPtr, nIntra);
738
- for (let k = 0; k < N; k++) {
739
- const rowK = [];
740
- for (let fi = 0; fi < nFrac; fi++) {
741
- const perCand = new Array(nCand);
742
- for (let c = 0; c < nCand; c++) {
743
- const o = ((k * nFrac + fi) * nCand + c) * 3;
744
- perCand[c] = { dR: intraV[o], dT: intraV[o + 1], dA: intraV[o + 2] };
745
- }
746
- rowK.push({ frac: intraFracs[fi], perCand });
747
- }
748
- intra.push(rowK);
749
- }
750
- }
751
-
752
- for (const p of [layPtr, candPtr, fracPtr, basePtr, gapPtr, intraPtr]) this.free(p);
753
- return { R, T, A, gaps, intra, N };
754
- }
755
- }
756
-
757
- /** Instantiate from raw bytes (ArrayBuffer / Uint8Array). Sets the singleton. */
758
- export async function instantiateTmmWasm(bytes) {
759
- const { instance } = await WebAssembly.instantiate(
760
- bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), wasmImports());
761
- _instance = new TmmWasmInstance(instance);
762
- return _instance;
763
- }
764
-
765
- /** Renderer/Node helper: fetch the `.wasm` at `url` and instantiate it. */
766
- export function initTmmWasmFromUrl(url) {
767
- if (_initPromise) return _initPromise;
768
- _initPromise = (async () => {
769
- try {
770
- const resp = await fetch(url);
771
- if (!resp.ok) throw new Error(`fetch ${url} → ${resp.status}`);
772
- const buf = await resp.arrayBuffer();
773
- await instantiateTmmWasm(buf);
774
- return true;
775
- } catch (e) {
776
- // Not built yet / not found : silent fallback to JS.
777
- _instance = null;
778
- return false;
779
- }
780
- })();
781
- return _initPromise;
782
- }
783
-
784
- export function setTmmWasmEnabled(on) { _enabled = !!on; }
785
- /** TEST-ONLY: inject a TmmWasmInstance (or null) directly, bypassing the .wasm
786
- * fetch/instantiate, so the integration seam can be exercised with a mock. */
787
- export function __setTmmWasmInstanceForTest(inst) { _instance = inst; }
788
-
789
- // ── Cross-thread plumbing ────────────────────────────────────────────────────
790
- // The renderer (main thread) loads the .wasm bytes once via IPC, instantiates
791
- // its own module, and BROADCASTS the same bytes to each pool/worker (workers
792
- // can't fetch a file:// asset under contextIsolation). Each worker instantiates
793
- // its OWN module (no shared memory) and enables the flag.
794
-
795
- let _workerBytes = null; // main-side: raw bytes to hand to workers
796
- let _workerInitPromise = null; // worker-side: in-flight instantiation
797
-
798
- /**
799
- * MAIN THREAD bootstrap. Store the bytes for worker broadcast and, if the user
800
- * enabled the feature, instantiate the main-thread module and flip the flag.
801
- * Safe to call once at startup; failures fall back to JS silently.
802
- */
803
- export async function initTmmWasmMainThread(bytes, enabled) {
804
- if (bytes) _workerBytes = bytes; // remember for workers + later toggles
805
- if (!enabled) { _enabled = false; return false; } // toggle off → JS everywhere
806
- if (!_workerBytes) return false; // artifact never loaded
807
- if (!_instance) { // instantiate once; reuse on re-toggle
808
- try { await instantiateTmmWasm(_workerBytes); }
809
- catch (_) { _instance = null; _enabled = false; return false; }
810
- }
811
- _enabled = true;
812
- return true;
813
- }
814
-
815
- /** MAIN THREAD: bytes to ship to a worker : only when the feature is active. */
816
- export function getTmmWasmBytesForWorker() {
817
- return (_enabled && _workerBytes) ? _workerBytes : null;
818
- }
819
-
820
- /**
821
- * WORKER side: kick off one-time instantiation from broadcast bytes and enable
822
- * the flag in this worker. Idempotent; no-op without bytes or once instantiated.
823
- */
824
- export function noteTmmWasmBytes(bytes) {
825
- if (!bytes || _instance || _workerInitPromise) return;
826
- _workerInitPromise = instantiateTmmWasm(bytes)
827
- .then(() => { _enabled = true; return true; })
828
- .catch(() => { _instance = null; _enabled = false; return false; });
829
- }
830
-
831
- /** WORKER side: await any in-flight instantiation before processing a job. */
832
- export function awaitTmmWasmReady() {
833
- return _workerInitPromise || Promise.resolve(_instance !== null);
834
- }
835
- export function isTmmWasmEnabled() { return _enabled; }
836
- export function isTmmWasmReady() { return _instance !== null; }
837
- /** Active iff the feature flag is on AND a module is instantiated. */
838
- export function tmmWasmActive() { return _enabled && _instance !== null; }
839
- export function getTmmWasm() { return _instance; }
1
+ /**
2
+ * tmmWasm.js : loader and ergonomic wrappers for the WebAssembly TMM kernel.
3
+ *
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
7
+ * mirror the JS functions.
8
+ *
9
+ * Acceleration is opt-in and falls back to JavaScript: if the `.wasm` is
10
+ * unavailable, instantiation fails, or the feature flag is off, the wrappers
11
+ * return `null` and callers use the JS path. Results are identical either way
12
+ * to float64 round-off.
13
+ *
14
+ * Instances are not shared across threads : there is no shared memory, so each
15
+ * context instantiates its own from the same bytes. Use `instantiateTmmWasm()`
16
+ * where you already hold the bytes (a worker receives them in its init message)
17
+ * and `initTmmWasmFromUrl()` where the artifact is fetchable.
18
+ */
19
+
20
+ import { omegaFromLambdaNm } from './phase.js';
21
+
22
+ let _instance = null; // TmmWasmInstance | null
23
+ let _enabled = false; // feature flag (default OFF)
24
+ let _initPromise = null; // de-dupe concurrent init
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
+
52
+ // Permissive imports: a STANDALONE_WASM build of pure-math C usually needs no
53
+ // imports, but ALLOW_MEMORY_GROWTH may emit `emscripten_notify_memory_growth`,
54
+ // and some toolchains emit WASI stubs. Cover them so instantiation never throws.
55
+ function wasmImports() {
56
+ return {
57
+ env: { emscripten_notify_memory_growth: () => {} },
58
+ wasi_snapshot_preview1: new Proxy({}, { get: () => () => 0 }),
59
+ };
60
+ }
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
+
71
+ export class TmmWasmInstance {
72
+ constructor(instance) {
73
+ const ex = instance.exports;
74
+ this.exports = ex;
75
+ // STANDALONE_WASM reactor modules expose an initializer that must run
76
+ // before malloc (sets up the allocator + any static ctors). Call it once.
77
+ if (typeof ex._initialize === 'function') ex._initialize();
78
+ else if (typeof ex.__wasm_call_ctors === 'function') ex.__wasm_call_ctors();
79
+ this.memory = ex.memory;
80
+ this.malloc = ex.malloc || ex._malloc;
81
+ this.free = ex.free || ex._free;
82
+ this._tmm_one = ex.tmm_one || ex._tmm_one;
83
+ this._tmm_spectrum = ex.tmm_spectrum || ex._tmm_spectrum;
84
+ this._tmm_jacobian = ex.tmm_jacobian || ex._tmm_jacobian;
85
+ this._tmm_needle_scan = ex.tmm_needle_scan || ex._tmm_needle_scan;
86
+ // Optional (added later for SQP/Newton accel): a .wasm built before the
87
+ // Hessian kernel existed simply lacks it → callers fall back to JS.
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 batched phase Jacobian arrived after the
95
+ // three above, for fitting a whole measured Ψ/Δ or group-delay spectrum.
96
+ this._tmm_phase_jacobian_spectrum = ex.tmm_phase_jacobian_spectrum
97
+ || ex._tmm_phase_jacobian_spectrum || null;
98
+ // Optional, same reason: the growing-stack kernels (monitor curve and
99
+ // per-step deposition spectra) arrived after all of the above.
100
+ this._tmm_monitor_curve = ex.tmm_monitor_curve || ex._tmm_monitor_curve || null;
101
+ this._tmm_deposition_spectra = ex.tmm_deposition_spectra || ex._tmm_deposition_spectra || null;
102
+ // Optional, same reason: the persistent growing-layer evaluator
103
+ // (wavelength-grid-per-call) arrived after the two kernels above.
104
+ this._growing_eval_create = ex.tmm_growing_eval_create || ex._tmm_growing_eval_create || null;
105
+ this._growing_eval_set_top = ex.tmm_growing_eval_set_top || ex._tmm_growing_eval_set_top || null;
106
+ this._growing_eval_sample = ex.tmm_growing_eval_sample || ex._tmm_growing_eval_sample || null;
107
+ this._growing_eval_free = ex.tmm_growing_eval_free || ex._tmm_growing_eval_free || null;
108
+ const missingExports = !this.malloc || !this.free || !this._tmm_one ||
109
+ !this._tmm_spectrum || !this._tmm_jacobian || !this._tmm_needle_scan;
110
+ if (missingExports) {
111
+ throw new Error('tmmWasm: required exports missing from module');
112
+ }
113
+ this._scratchPtr = 0; // persistent per-call scratch arena (lazy)
114
+ this._scratchN = 0;
115
+ }
116
+
117
+ _alloc(nDoubles) {
118
+ const ptr = this.malloc(nDoubles * 8);
119
+ if (!ptr) throw new Error('tmmWasm: malloc failed');
120
+ return ptr;
121
+ }
122
+ // Fresh view : memory.buffer is detached after any growth, so re-create
123
+ // views AFTER all mallocs for a call are done.
124
+ _view(ptr, nDoubles) {
125
+ return new Float64Array(this.memory.buffer, ptr, nDoubles);
126
+ }
127
+
128
+ // Persistent scratch arena for the per-call hot paths (tmmOne/tmmJacobian).
129
+ // These are invoked thousands of times per optimization run; malloc/free +
130
+ // typed-array churn per call would dominate the JS↔WASM boundary cost and
131
+ // can make per-call WASM SLOWER than JS. Reusing one buffer (grown on demand,
132
+ // never freed between calls) makes each call just write-args / read-result.
133
+ _scratch(nDoubles) {
134
+ if (this._scratchN < nDoubles) {
135
+ if (this._scratchPtr) this.free(this._scratchPtr);
136
+ this._scratchPtr = this._alloc(nDoubles);
137
+ this._scratchN = nDoubles;
138
+ }
139
+ return this._scratchPtr;
140
+ }
141
+
142
+ /**
143
+ * Single (λ, θ, pol) : mirrors tmm() in thinFilmMath.js.
144
+ * @returns {{R:number,T:number,A:number}}
145
+ */
146
+ tmmOne(lambda_nm, theta_deg, polCode /* 0=s,1=p */, n0, ns, layers) {
147
+ const N = layers.length;
148
+ const need = 3 * N + 3; // layers [0..3N) + out [3N..3N+3)
149
+ const ptr = this._scratch(need); // may grow→detach; view created AFTER
150
+ const buf = this._view(ptr, need);
151
+ for (let i = 0; i < N; i++) {
152
+ buf[3 * i + 0] = layers[i].n[0];
153
+ buf[3 * i + 1] = layers[i].n[1];
154
+ buf[3 * i + 2] = layers[i].d;
155
+ }
156
+ const outPtr = ptr + 3 * N * 8;
157
+ this._tmm_one(lambda_nm, theta_deg, polCode | 0,
158
+ n0[0], n0[1], ns[0], ns[1], ptr, N, outPtr);
159
+ return { R: buf[3 * N], T: buf[3 * N + 1], A: buf[3 * N + 2] };
160
+ }
161
+
162
+ /**
163
+ * Batched spectrum over a λ grid for BOTH polarizations : the boundary-
164
+ * amortizing path behind evaluateSpectrum().
165
+ * @param {number[]} lambdas
166
+ * @param {[number,number][]} n0List incident ñ per λ
167
+ * @param {[number,number][]} nsList substrate ñ per λ
168
+ * @param {[number,number][][]} layerNK [layer][λ] = ñ
169
+ * @param {number[]} thick layer thicknesses (nm), length N
170
+ * @param {number} theta_deg
171
+ * @returns {{Rs,Ts,As,Rp,Tp,Ap}} each a Float64Array(nLam)
172
+ */
173
+ tmmSpectrum(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
174
+ const nLam = lambdas.length;
175
+ const N = thick.length;
176
+
177
+ const lamPtr = this._alloc(nLam);
178
+ const n0Ptr = this._alloc(2 * nLam);
179
+ const nsPtr = this._alloc(2 * nLam);
180
+ const mPtr = this._alloc(Math.max(1, 2 * N * nLam));
181
+ const thPtr = this._alloc(Math.max(1, N));
182
+ const rsPtr = this._alloc(nLam), tsPtr = this._alloc(nLam), asPtr = this._alloc(nLam);
183
+ const rpPtr = this._alloc(nLam), tpPtr = this._alloc(nLam), apPtr = this._alloc(nLam);
184
+
185
+ // Views created after all mallocs (buffer may have grown/detached).
186
+ const lam = this._view(lamPtr, nLam);
187
+ const n0v = this._view(n0Ptr, 2 * nLam);
188
+ const nsv = this._view(nsPtr, 2 * nLam);
189
+ const mv = this._view(mPtr, Math.max(1, 2 * N * nLam));
190
+ const thv = this._view(thPtr, Math.max(1, N));
191
+ for (let i = 0; i < nLam; i++) {
192
+ lam[i] = lambdas[i];
193
+ n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
194
+ nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
195
+ }
196
+ for (let k = 0; k < N; k++) {
197
+ thv[k] = thick[k];
198
+ const row = layerNK[k];
199
+ const base = k * nLam * 2;
200
+ for (let i = 0; i < nLam; i++) {
201
+ mv[base + 2 * i] = row[i][0];
202
+ mv[base + 2 * i + 1] = row[i][1];
203
+ }
204
+ }
205
+
206
+ this._tmm_spectrum(lamPtr, nLam, n0Ptr, nsPtr, mPtr, thPtr, N, theta_deg,
207
+ rsPtr, tsPtr, asPtr, rpPtr, tpPtr, apPtr);
208
+
209
+ // Copy outputs out of wasm memory before freeing.
210
+ const cp = (p) => Float64Array.from(this._view(p, nLam));
211
+ const res = { Rs: cp(rsPtr), Ts: cp(tsPtr), As: cp(asPtr),
212
+ Rp: cp(rpPtr), Tp: cp(tpPtr), Ap: cp(apPtr) };
213
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr,
214
+ rsPtr, tsPtr, asPtr, rpPtr, tpPtr, apPtr]) this.free(p);
215
+ return res;
216
+ }
217
+
218
+ /** True if the loaded module carries the growing-stack kernels. */
219
+ hasGrowingKernels() {
220
+ return !!(this._tmm_monitor_curve && this._tmm_deposition_spectra);
221
+ }
222
+
223
+ /** True if the loaded module carries the persistent growing-layer evaluator. */
224
+ hasGrowingEval() {
225
+ return !!(this._growing_eval_create && this._growing_eval_set_top
226
+ && this._growing_eval_sample && this._growing_eval_free);
227
+ }
228
+
229
+ /**
230
+ * Monitor curve of one growing layer at one wavelength: the completed
231
+ * stack's matrix is built once, then every sample thickness costs one 2×2
232
+ * multiply. Returns the forward and substrate-side passes of the coated
233
+ * surface, both polarizations; the caller does the incoherent slab
234
+ * combination (or forms A = 1−R−T for a semi-infinite substrate).
235
+ * @param {[number,number]} n0 incident ñ
236
+ * @param {[number,number]} ns substrate ñ
237
+ * @param {{n:[number,number],d:number}[]} baseLayers completed stack,
238
+ * outermost first
239
+ * @param {[number,number]} ngNK growing layer ñ
240
+ * @param {ArrayLike<number>} dArr sample thicknesses (nm)
241
+ * @returns {{Rs,Ts,Rp,Tp,Rrs,Rrp}} each a Float64Array(dArr.length)
242
+ */
243
+ monitorCurve(lambda_nm, theta_deg, n0, ns, baseLayers, ngNK, dArr) {
244
+ const NB = baseLayers.length;
245
+ const nD = dArr.length;
246
+ // arena: base[3NB] | d[nD] | Rs,Ts,Rp,Tp,Rrs,Rrp [6·nD]
247
+ const oBase = 0, oD = 3 * NB, oOut = oD + nD;
248
+ const need = oOut + 6 * nD;
249
+ const ptr = this._scratch(need);
250
+ const buf = this._view(ptr, need);
251
+ for (let i = 0; i < NB; i++) {
252
+ buf[3 * i + 0] = baseLayers[i].n[0];
253
+ buf[3 * i + 1] = baseLayers[i].n[1];
254
+ buf[3 * i + 2] = baseLayers[i].d;
255
+ }
256
+ for (let k = 0; k < nD; k++) buf[oD + k] = dArr[k];
257
+ const P = (off) => ptr + off * 8;
258
+ this._tmm_monitor_curve(lambda_nm, theta_deg,
259
+ n0[0], n0[1], ns[0], ns[1], P(oBase), NB, ngNK[0], ngNK[1],
260
+ P(oD), nD,
261
+ P(oOut), P(oOut + nD), P(oOut + 2 * nD), P(oOut + 3 * nD),
262
+ P(oOut + 4 * nD), P(oOut + 5 * nD));
263
+ // Fresh view after the call: memory growth detaches the old buffer.
264
+ const out = this._view(ptr, need);
265
+ return {
266
+ Rs: out.slice(oOut, oOut + nD),
267
+ Ts: out.slice(oOut + nD, oOut + 2 * nD),
268
+ Rp: out.slice(oOut + 2 * nD, oOut + 3 * nD),
269
+ Tp: out.slice(oOut + 3 * nD, oOut + 4 * nD),
270
+ Rrs: out.slice(oOut + 4 * nD, oOut + 5 * nD),
271
+ Rrp: out.slice(oOut + 5 * nD, oOut + 6 * nD),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Per-step spectra of a growing stack: one call returns the forward and
277
+ * substrate-side passes of the coated surface after every deposited layer,
278
+ * so all N step spectra cost about what the final one costs alone. Layers
279
+ * in DEPOSITION order (first deposited first).
280
+ * @param {number[]} lambdas
281
+ * @param {[number,number][]} n0List incident ñ per λ
282
+ * @param {[number,number][]} nsList substrate ñ per λ
283
+ * @param {[number,number][][]} layerNK [layer][λ] = ñ, deposition order
284
+ * @param {number[]} thick thicknesses (nm); 0 repeats the previous step
285
+ * @param {number} theta_deg
286
+ * @returns {{Rs,Ts,Rp,Tp,Rrs,Rrp}} each a Float64Array(N · nLam),
287
+ * step-major: value for step k at wavelength i is at [k·nLam + i]
288
+ */
289
+ depositionSpectra(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
290
+ const nLam = lambdas.length;
291
+ const N = thick.length;
292
+ const nOut = Math.max(1, N * nLam);
293
+
294
+ const lamPtr = this._alloc(nLam);
295
+ const n0Ptr = this._alloc(2 * nLam);
296
+ const nsPtr = this._alloc(2 * nLam);
297
+ const mPtr = this._alloc(Math.max(1, 2 * N * nLam));
298
+ const thPtr = this._alloc(Math.max(1, N));
299
+ const outPtrs = Array.from({ length: 6 }, () => this._alloc(nOut));
300
+
301
+ const lam = this._view(lamPtr, nLam);
302
+ const n0v = this._view(n0Ptr, 2 * nLam);
303
+ const nsv = this._view(nsPtr, 2 * nLam);
304
+ const mv = this._view(mPtr, Math.max(1, 2 * N * nLam));
305
+ const thv = this._view(thPtr, Math.max(1, N));
306
+ for (let i = 0; i < nLam; i++) {
307
+ lam[i] = lambdas[i];
308
+ n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
309
+ nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
310
+ }
311
+ for (let k = 0; k < N; k++) {
312
+ thv[k] = thick[k];
313
+ const row = layerNK[k];
314
+ const base = k * nLam * 2;
315
+ for (let i = 0; i < nLam; i++) {
316
+ mv[base + 2 * i] = row[i][0];
317
+ mv[base + 2 * i + 1] = row[i][1];
318
+ }
319
+ }
320
+
321
+ const ok = this._tmm_deposition_spectra(lamPtr, nLam, n0Ptr, nsPtr, mPtr, thPtr, N,
322
+ theta_deg, ...outPtrs);
323
+ if (!ok) {
324
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr, ...outPtrs]) this.free(p);
325
+ // The outputs were never written; returning them would hand the
326
+ // caller uninitialized memory as spectra.
327
+ throw new Error('tmmWasm: deposition-spectra state allocation failed');
328
+ }
329
+
330
+ const cp = (p) => Float64Array.from(this._view(p, nOut));
331
+ const [Rs, Ts, Rp, Tp, Rrs, Rrp] = outPtrs.map(cp);
332
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr, ...outPtrs]) this.free(p);
333
+ return { Rs, Ts, Rp, Tp, Rrs, Rrp };
334
+ }
335
+
336
+ /**
337
+ * Persistent growing-layer evaluator: the completed stack's products for
338
+ * the whole wavelength grid are folded once and kept in kernel memory;
339
+ * each sample() then answers one thickness of the growing layer across
340
+ * the grid. This is the shape a broadband monitor scan needs (a spectrum
341
+ * per scan while the layers beneath stay fixed), where monitorCurve is
342
+ * the single-λ, many-thicknesses shape.
343
+ *
344
+ * The completed stack arrives OUTERMOST FIRST, layer-major like
345
+ * depositionSpectra's matNK; zero-thickness entries are skipped.
346
+ *
347
+ * The returned handle owns kernel memory: call free() when the layer is
348
+ * done. A dropped handle is reclaimed by a finalizer eventually, but
349
+ * deterministic free() is what keeps a long run's footprint flat.
350
+ *
351
+ * @param {number[]} lambdas
352
+ * @param {[number,number][]} n0List incident ñ per λ
353
+ * @param {[number,number][]} nsList substrate ñ per λ
354
+ * @param {[number,number][][]} layerNK [layer][λ] = ñ, outermost first
355
+ * @param {number[]} thick completed thicknesses (nm)
356
+ * @param {number} theta_deg
357
+ * @returns {{setTop, sample, free}} setTop(ngList) declares the growing
358
+ * layer's ñ per λ; sample(d, out?) returns {Rs,Ts,Rp,Tp,Rrs,Rrp}
359
+ * (each Float64Array(nLam), written into `out` when given, so a
360
+ * scan loop can reuse one set of buffers).
361
+ */
362
+ growingEval(lambdas, n0List, nsList, layerNK, thick, theta_deg) {
363
+ if (!this.hasGrowingEval()) throw new Error('tmmWasm: growing evaluator kernel not in this build');
364
+ const nLam = lambdas.length;
365
+ const NB = thick.length;
366
+
367
+ const lamPtr = this._alloc(nLam);
368
+ const n0Ptr = this._alloc(2 * nLam);
369
+ const nsPtr = this._alloc(2 * nLam);
370
+ const mPtr = this._alloc(Math.max(1, 2 * NB * nLam));
371
+ const thPtr = this._alloc(Math.max(1, NB));
372
+ const lam = this._view(lamPtr, nLam);
373
+ const n0v = this._view(n0Ptr, 2 * nLam);
374
+ const nsv = this._view(nsPtr, 2 * nLam);
375
+ const mv = this._view(mPtr, Math.max(1, 2 * NB * nLam));
376
+ const thv = this._view(thPtr, Math.max(1, NB));
377
+ for (let i = 0; i < nLam; i++) {
378
+ lam[i] = lambdas[i];
379
+ n0v[2 * i] = n0List[i][0]; n0v[2 * i + 1] = n0List[i][1];
380
+ nsv[2 * i] = nsList[i][0]; nsv[2 * i + 1] = nsList[i][1];
381
+ }
382
+ for (let k = 0; k < NB; k++) {
383
+ thv[k] = thick[k];
384
+ const row = layerNK[k];
385
+ const base = k * nLam * 2;
386
+ for (let i = 0; i < nLam; i++) {
387
+ mv[base + 2 * i] = row[i][0];
388
+ mv[base + 2 * i + 1] = row[i][1];
389
+ }
390
+ }
391
+ const handlePtr = this._growing_eval_create(lamPtr, nLam, theta_deg,
392
+ n0Ptr, nsPtr, mPtr, thPtr, NB);
393
+ for (const p of [lamPtr, n0Ptr, nsPtr, mPtr, thPtr]) this.free(p);
394
+ if (!handlePtr) throw new Error('tmmWasm: growing evaluator allocation failed');
395
+
396
+ const self = this;
397
+ const handle = {
398
+ ptr: handlePtr,
399
+ setTop(ngList) {
400
+ if (!this.ptr) throw new Error('tmmWasm: growing evaluator already freed');
401
+ const p = self._scratch(2 * nLam);
402
+ const buf = self._view(p, 2 * nLam);
403
+ for (let i = 0; i < nLam; i++) {
404
+ buf[2 * i] = ngList[i][0]; buf[2 * i + 1] = ngList[i][1];
405
+ }
406
+ self._growing_eval_set_top(this.ptr, p);
407
+ },
408
+ sample(d, out = null) {
409
+ if (!this.ptr) throw new Error('tmmWasm: growing evaluator already freed');
410
+ const p = self._scratch(6 * nLam);
411
+ const P = (block) => p + block * nLam * 8;
412
+ const ok = self._growing_eval_sample(this.ptr, d,
413
+ P(0), P(1), P(2), P(3), P(4), P(5));
414
+ // The kernel writes nothing without a declared growing layer;
415
+ // copying the arena anyway would hand back stale memory.
416
+ if (!ok) throw new Error('tmmWasm: growing evaluator sampled before setTop');
417
+ // Fresh view AFTER the call (memory may have grown → detach).
418
+ const buf = self._view(p, 6 * nLam);
419
+ if (!out) {
420
+ out = {
421
+ Rs: new Float64Array(nLam), Ts: new Float64Array(nLam),
422
+ Rp: new Float64Array(nLam), Tp: new Float64Array(nLam),
423
+ Rrs: new Float64Array(nLam), Rrp: new Float64Array(nLam),
424
+ };
425
+ }
426
+ out.Rs.set(buf.subarray(0, nLam));
427
+ out.Ts.set(buf.subarray(nLam, 2 * nLam));
428
+ out.Rp.set(buf.subarray(2 * nLam, 3 * nLam));
429
+ out.Tp.set(buf.subarray(3 * nLam, 4 * nLam));
430
+ out.Rrs.set(buf.subarray(4 * nLam, 5 * nLam));
431
+ out.Rrp.set(buf.subarray(5 * nLam, 6 * nLam));
432
+ return out;
433
+ },
434
+ free() {
435
+ if (!this.ptr) return;
436
+ growingEvalFinalizer?.unregister(this);
437
+ self._growing_eval_free(this.ptr);
438
+ this.ptr = 0;
439
+ },
440
+ };
441
+ growingEvalFinalizer?.register(handle,
442
+ { wasm: this, ptr: handlePtr }, handle);
443
+ return handle;
444
+ }
445
+
446
+ /**
447
+ * Analytic thickness Jacobian for one (λ, θ, pol) : mirrors
448
+ * tmmThicknessJacobian(). layers used AS-IS (index parity).
449
+ * @returns {{R,T,A, dRdd:Float64Array, dTdd, dAdd, N}}
450
+ */
451
+ tmmJacobian(lambda_nm, theta_deg, polCode, n0, ns, layers) {
452
+ const N = layers.length;
453
+ const M = Math.max(1, N);
454
+ // arena layout: layers[3N] | dRdd[M] | dTdd[M] | dAdd[M] | base[3]
455
+ const oLay = 0, oDR = 3 * N, oDT = oDR + M, oDA = oDT + M, oBase = oDA + M;
456
+ const need = oBase + 3;
457
+ const ptr = this._scratch(need);
458
+ const buf = this._view(ptr, need);
459
+ for (let i = 0; i < N; i++) {
460
+ buf[3 * i + 0] = layers[i].n[0];
461
+ buf[3 * i + 1] = layers[i].n[1];
462
+ buf[3 * i + 2] = layers[i].d;
463
+ }
464
+ const P = (off) => ptr + off * 8;
465
+ this._tmm_jacobian(lambda_nm, theta_deg, polCode | 0,
466
+ n0[0], n0[1], ns[0], ns[1], P(oLay), N, P(oDR), P(oDT), P(oDA), P(oBase));
467
+ // Re-create the view AFTER the kernel call (like tmmSpectrum): under
468
+ // ALLOW_MEMORY_GROWTH the kernel may grow wasm memory, which detaches the
469
+ // ArrayBuffer `buf` was created over : reading the stale `buf` then yields
470
+ // garbage / throws. `out` is a fresh view over the current buffer.
471
+ const out = this._view(ptr, need);
472
+ return {
473
+ R: out[oBase], T: out[oBase + 1], A: out[oBase + 2], N,
474
+ dRdd: out.slice(oDR, oDR + N),
475
+ dTdd: out.slice(oDT, oDT + N),
476
+ dAdd: out.slice(oDA, oDA + N),
477
+ };
478
+ }
479
+
480
+ /** True if the loaded module carries the Hessian kernel (newer build). */
481
+ hasHessian() { return !!this._tmm_hessian; }
482
+
483
+ /**
484
+ * Analytic thickness Hessian for one (λ, θ, pol) : mirrors
485
+ * tmmThicknessHessian(). Returns first AND second derivatives; the N×N
486
+ * second-derivative blocks are reshaped into nested arrays (one Float64Array
487
+ * row per layer, FULL symmetric) so the shape matches the JS oracle exactly.
488
+ * @returns {{R,T,A, dRdd, dTdd, dAdd, d2Rdd, d2Tdd, d2Add, N}}
489
+ */
490
+ tmmHessian(lambda_nm, theta_deg, polCode, n0, ns, layers) {
491
+ const N = layers.length;
492
+ const M = Math.max(1, N);
493
+ const NN = Math.max(1, N * N);
494
+ // arena: layers[3N] | dRdd[M] | dTdd[M] | dAdd[M] | d2R[NN] | d2T[NN] | d2A[NN] | base[3]
495
+ const oLay = 0, oDR = 3 * N, oDT = oDR + M, oDA = oDT + M,
496
+ oR2 = oDA + M, oT2 = oR2 + NN, oA2 = oT2 + NN, oBase = oA2 + NN;
497
+ const need = oBase + 3;
498
+ const ptr = this._scratch(need);
499
+ const buf = this._view(ptr, need);
500
+ for (let i = 0; i < N; i++) {
501
+ buf[3 * i + 0] = layers[i].n[0];
502
+ buf[3 * i + 1] = layers[i].n[1];
503
+ buf[3 * i + 2] = layers[i].d;
504
+ }
505
+ const P = (off) => ptr + off * 8;
506
+ this._tmm_hessian(lambda_nm, theta_deg, polCode | 0,
507
+ n0[0], n0[1], ns[0], ns[1], P(oLay), N,
508
+ P(oDR), P(oDT), P(oDA), P(oR2), P(oT2), P(oA2), P(oBase));
509
+ // Fresh view after the call (memory may have grown buf detached).
510
+ const out = this._view(ptr, need);
511
+ const reshape = (off) => {
512
+ const rows = new Array(N);
513
+ for (let i = 0; i < N; i++) rows[i] = out.slice(off + i * N, off + i * N + N);
514
+ return rows;
515
+ };
516
+ return {
517
+ R: out[oBase], T: out[oBase + 1], A: out[oBase + 2], N,
518
+ dRdd: out.slice(oDR, oDR + N),
519
+ dTdd: out.slice(oDT, oDT + N),
520
+ dAdd: out.slice(oDA, oDA + N),
521
+ d2Rdd: reshape(oR2), d2Tdd: reshape(oT2), d2Add: reshape(oA2),
522
+ };
523
+ }
524
+
525
+ /** True if the loaded module carries the phase-dispersion kernel. */
526
+ hasPhase() { return !!this._tmm_phase_one; }
527
+
528
+ /**
529
+ * Phase, group delay, GDD and TOD at one wavelength : mirrors
530
+ * tmmPhaseDispersion() in phase.js.
531
+ *
532
+ * @param {number[][]} n0Jet incident-medium index jet, 4 × [re, im]
533
+ * @param {number[][]} nsJet substrate index jet
534
+ * @param {{nJet:number[][], d:number}[]} layers
535
+ * @param {{omega?:number, sinTheta0Jet?:number[][]}} [options]
536
+ * @returns {{r: object|null, t: object|null}}
537
+ */
538
+ tmmPhaseOne(lambda_nm, theta_deg, polCode, n0Jet, nsJet, layers, options = {}) {
539
+ const N = layers.length;
540
+ const omega = options.omega ?? omegaFromLambdaNm(lambda_nm);
541
+ const sinJet = options.sinTheta0Jet || null;
542
+ // arena: layerJets[8N] | thick[N] | n0[8] | ns[8] | sin[8] | out[10]
543
+ const oLay = 0, oThick = 8 * N, oN0 = oThick + N, oNs = oN0 + 8,
544
+ oSin = oNs + 8, oOut = oSin + 8;
545
+ const need = oOut + 10;
546
+ const ptr = this._scratch(need);
547
+ const buf = this._view(ptr, need);
548
+ for (let i = 0; i < N; i++) {
549
+ writeJet(buf, oLay + 8 * i, layers[i].nJet);
550
+ buf[oThick + i] = layers[i].d;
551
+ }
552
+ writeJet(buf, oN0, n0Jet);
553
+ writeJet(buf, oNs, nsJet);
554
+ if (sinJet) writeJet(buf, oSin, sinJet);
555
+ const P = (off) => ptr + off * 8;
556
+ this._tmm_phase_one(lambda_nm, omega, theta_deg, polCode | 0,
557
+ P(oN0), P(oNs), P(oLay), P(oThick), N, sinJet ? P(oSin) : 0, P(oOut));
558
+ const out = this._view(ptr, need);
559
+ return { r: readPhase(out, oOut), t: readPhase(out, oOut + 5) };
560
+ }
561
+
562
+ /**
563
+ * Batched phase dispersion over a λ grid : the boundary-amortizing path.
564
+ *
565
+ * Unlike `tmmSpectrum` this takes one polarization, because the kernel is an
566
+ * order of magnitude dearer per sample and callers at normal incidence would
567
+ * otherwise pay twice for the same numbers.
568
+ *
569
+ * @param {number[]} lambdas
570
+ * @param {number[][][]} n0Jets index jet per λ
571
+ * @param {number[][][]} nsJets index jet per λ
572
+ * @param {number[][][][]} layerJets [layer][λ] = index jet
573
+ * @param {number[]} thick layer thicknesses (nm), length N
574
+ * @param {{omegas?:number[], sinJets?:number[][][]}} [options]
575
+ * @returns {{r: object, t: object}} each `{phaseRad, gd, gdd, tod,
576
+ * magnitudeSquared}` of Float64Array(nLam). Failed samples hold NaN.
577
+ */
578
+ tmmPhaseSpectrum(lambdas, n0Jets, nsJets, layerJets, thick, theta_deg, polCode, options = {}) {
579
+ const nLam = lambdas.length;
580
+ const N = thick.length;
581
+ const omegas = options.omegas
582
+ || lambdas.map(lambda => omegaFromLambdaNm(lambda));
583
+ const sinJets = options.sinJets || null;
584
+
585
+ const lamPtr = this._alloc(nLam);
586
+ const omPtr = this._alloc(nLam);
587
+ const n0Ptr = this._alloc(8 * nLam);
588
+ const nsPtr = this._alloc(8 * nLam);
589
+ const matPtr = this._alloc(Math.max(1, 8 * N * nLam));
590
+ const thPtr = this._alloc(Math.max(1, N));
591
+ const sinPtr = sinJets ? this._alloc(8 * nLam) : 0;
592
+ const outPtr = this._alloc(10 * nLam);
593
+
594
+ // Views created after all mallocs (the buffer may have grown/detached).
595
+ const lam = this._view(lamPtr, nLam);
596
+ const om = this._view(omPtr, nLam);
597
+ const n0v = this._view(n0Ptr, 8 * nLam);
598
+ const nsv = this._view(nsPtr, 8 * nLam);
599
+ const matv = this._view(matPtr, Math.max(1, 8 * N * nLam));
600
+ const thv = this._view(thPtr, Math.max(1, N));
601
+ const sinv = sinJets ? this._view(sinPtr, 8 * nLam) : null;
602
+ for (let i = 0; i < nLam; i++) {
603
+ lam[i] = lambdas[i];
604
+ om[i] = omegas[i];
605
+ writeJet(n0v, 8 * i, n0Jets[i]);
606
+ writeJet(nsv, 8 * i, nsJets[i]);
607
+ if (sinv) writeJet(sinv, 8 * i, sinJets[i]);
608
+ }
609
+ for (let k = 0; k < N; k++) {
610
+ thv[k] = thick[k];
611
+ const row = layerJets[k];
612
+ const base = k * nLam * 8;
613
+ for (let i = 0; i < nLam; i++) writeJet(matv, base + 8 * i, row[i]);
614
+ }
615
+
616
+ this._tmm_phase_spectrum(lamPtr, omPtr, nLam, n0Ptr, nsPtr, matPtr, thPtr, N,
617
+ theta_deg, polCode | 0, sinPtr, outPtr);
618
+
619
+ // De-interleave into one array per quantity before freeing.
620
+ const out = this._view(outPtr, 10 * nLam);
621
+ const side = (base) => {
622
+ const q = {
623
+ phaseRad: new Float64Array(nLam), gd: new Float64Array(nLam),
624
+ gdd: new Float64Array(nLam), tod: new Float64Array(nLam),
625
+ magnitudeSquared: new Float64Array(nLam),
626
+ };
627
+ const keys = ['phaseRad', 'gd', 'gdd', 'tod', 'magnitudeSquared'];
628
+ for (let i = 0; i < nLam; i++) {
629
+ for (let j = 0; j < 5; j++) q[keys[j]][i] = out[10 * i + base + j];
630
+ }
631
+ return q;
632
+ };
633
+ const result = { r: side(0), t: side(5) };
634
+ for (const p of [lamPtr, omPtr, n0Ptr, nsPtr, matPtr, thPtr, outPtr]) this.free(p);
635
+ if (sinPtr) this.free(sinPtr);
636
+ return result;
637
+ }
638
+
639
+ /**
640
+ * Phase dispersion plus exact thickness derivatives : mirrors
641
+ * tmmPhaseThicknessJacobian(). Layers used AS-IS (index parity).
642
+ *
643
+ * @returns {{r, t}} each the phase quantities plus `dPhaseDeg`, `dGd`,
644
+ * `dGdd`, `dTod` and `dLogMagnitudeSquared` as Float64Array(N), or `null`
645
+ * arrays on overflow.
646
+ */
647
+ tmmPhaseJacobian(lambda_nm, theta_deg, polCode, n0Jet, nsJet, layers, options = {}) {
648
+ const N = layers.length;
649
+ const M = Math.max(1, N);
650
+ const omega = options.omega ?? omegaFromLambdaNm(lambda_nm);
651
+ const sinJet = options.sinTheta0Jet || null;
652
+ // arena: layerJets[8N] | thick[N] | n0[8] | ns[8] | sin[8] | out[10] | deriv[10M]
653
+ const oLay = 0, oThick = 8 * N, oN0 = oThick + N, oNs = oN0 + 8,
654
+ oSin = oNs + 8, oOut = oSin + 8, oDeriv = oOut + 10;
655
+ const need = oDeriv + 10 * M;
656
+ const ptr = this._scratch(need);
657
+ const buf = this._view(ptr, need);
658
+ for (let i = 0; i < N; i++) {
659
+ writeJet(buf, oLay + 8 * i, layers[i].nJet);
660
+ buf[oThick + i] = layers[i].d;
661
+ }
662
+ writeJet(buf, oN0, n0Jet);
663
+ writeJet(buf, oNs, nsJet);
664
+ if (sinJet) writeJet(buf, oSin, sinJet);
665
+ const P = (off) => ptr + off * 8;
666
+ this._tmm_phase_jacobian(lambda_nm, omega, theta_deg, polCode | 0,
667
+ P(oN0), P(oNs), P(oLay), P(oThick), N, sinJet ? P(oSin) : 0,
668
+ P(oOut), P(oDeriv));
669
+ const out = this._view(ptr, need);
670
+ const side = (phaseBase, derivBase) => {
671
+ const base = readPhase(out, phaseBase);
672
+ if (!base) return null;
673
+ // The kernel fills the whole block with NaN when the matrix product
674
+ // overflowed and the prefix/suffix decomposition had to be abandoned.
675
+ const overflowed = N > 0 && Number.isNaN(out[oDeriv + derivBase * N]);
676
+ const take = (q) => overflowed
677
+ ? null
678
+ : out.slice(oDeriv + (derivBase + q) * N, oDeriv + (derivBase + q) * N + N);
679
+ return {
680
+ ...base,
681
+ dPhaseDeg: take(0), dGd: take(1), dGdd: take(2), dTod: take(3),
682
+ dLogMagnitudeSquared: take(4),
683
+ };
684
+ };
685
+ return { r: side(oOut, 0), t: side(oOut + 5, 5) };
686
+ }
687
+
688
+ /** True if the loaded module carries the batched phase Jacobian. */
689
+ hasPhaseJacobianSpectrum() { return !!this._tmm_phase_jacobian_spectrum; }
690
+
691
+ /**
692
+ * Batched phase Jacobian over a λ grid : tmmPhaseJacobian at every
693
+ * wavelength in one call, for fitting a whole measured spectrum of phase
694
+ * quantities. Arguments as tmmPhaseSpectrum.
695
+ *
696
+ * @returns {{r: object, t: object}} each the five Float64Array(nLam) of
697
+ * tmmPhaseSpectrum plus `dPhaseDeg`, `dGd`, `dGdd`, `dTod` and
698
+ * `dLogMagnitudeSquared` as Float64Array(nLam × N), the derivative for
699
+ * wavelength i and layer k at `[i * N + k]`. A wavelength whose matrix
700
+ * product overflowed holds NaN across its derivative block.
701
+ */
702
+ tmmPhaseJacobianSpectrum(lambdas, n0Jets, nsJets, layerJets, thick, theta_deg, polCode, options = {}) {
703
+ const nLam = lambdas.length;
704
+ const N = thick.length;
705
+ const omegas = options.omegas
706
+ || lambdas.map(lambda => omegaFromLambdaNm(lambda));
707
+ const sinJets = options.sinJets || null;
708
+ const nDeriv = Math.max(1, 10 * N * nLam);
709
+
710
+ const lamPtr = this._alloc(nLam);
711
+ const omPtr = this._alloc(nLam);
712
+ const n0Ptr = this._alloc(8 * nLam);
713
+ const nsPtr = this._alloc(8 * nLam);
714
+ const matPtr = this._alloc(Math.max(1, 8 * N * nLam));
715
+ const thPtr = this._alloc(Math.max(1, N));
716
+ const sinPtr = sinJets ? this._alloc(8 * nLam) : 0;
717
+ const outPtr = this._alloc(10 * nLam);
718
+ const derivPtr = this._alloc(nDeriv);
719
+
720
+ const lam = this._view(lamPtr, nLam);
721
+ const om = this._view(omPtr, nLam);
722
+ const n0v = this._view(n0Ptr, 8 * nLam);
723
+ const nsv = this._view(nsPtr, 8 * nLam);
724
+ const matv = this._view(matPtr, Math.max(1, 8 * N * nLam));
725
+ const thv = this._view(thPtr, Math.max(1, N));
726
+ const sinv = sinJets ? this._view(sinPtr, 8 * nLam) : null;
727
+ for (let i = 0; i < nLam; i++) {
728
+ lam[i] = lambdas[i];
729
+ om[i] = omegas[i];
730
+ writeJet(n0v, 8 * i, n0Jets[i]);
731
+ writeJet(nsv, 8 * i, nsJets[i]);
732
+ if (sinv) writeJet(sinv, 8 * i, sinJets[i]);
733
+ }
734
+ for (let k = 0; k < N; k++) {
735
+ thv[k] = thick[k];
736
+ const row = layerJets[k];
737
+ const base = k * nLam * 8;
738
+ for (let i = 0; i < nLam; i++) writeJet(matv, base + 8 * i, row[i]);
739
+ }
740
+
741
+ this._tmm_phase_jacobian_spectrum(lamPtr, omPtr, nLam, n0Ptr, nsPtr, matPtr, thPtr, N,
742
+ theta_deg, polCode | 0, sinPtr, outPtr, derivPtr);
743
+
744
+ // De-interleave: per λ the kernel writes 10 phase values and then a
745
+ // [side][quantity][layer] block of 10 × N derivatives.
746
+ const out = this._view(outPtr, 10 * nLam);
747
+ const deriv = this._view(derivPtr, nDeriv);
748
+ const side = (base, derivBase) => {
749
+ const q = {
750
+ phaseRad: new Float64Array(nLam), gd: new Float64Array(nLam),
751
+ gdd: new Float64Array(nLam), tod: new Float64Array(nLam),
752
+ magnitudeSquared: new Float64Array(nLam),
753
+ dPhaseDeg: new Float64Array(nLam * N), dGd: new Float64Array(nLam * N),
754
+ dGdd: new Float64Array(nLam * N), dTod: new Float64Array(nLam * N),
755
+ dLogMagnitudeSquared: new Float64Array(nLam * N),
756
+ };
757
+ const keys = ['phaseRad', 'gd', 'gdd', 'tod', 'magnitudeSquared'];
758
+ const derivKeys = ['dPhaseDeg', 'dGd', 'dGdd', 'dTod', 'dLogMagnitudeSquared'];
759
+ for (let i = 0; i < nLam; i++) {
760
+ for (let j = 0; j < 5; j++) q[keys[j]][i] = out[10 * i + base + j];
761
+ for (let j = 0; j < derivKeys.length; j++) {
762
+ const from = 10 * N * i + (derivBase + j) * N;
763
+ for (let k = 0; k < N; k++) q[derivKeys[j]][i * N + k] = deriv[from + k];
764
+ }
765
+ }
766
+ return q;
767
+ };
768
+ const result = { r: side(0, 0), t: side(5, 5) };
769
+ for (const p of [lamPtr, omPtr, n0Ptr, nsPtr, matPtr, thPtr, outPtr, derivPtr]) this.free(p);
770
+ if (sinPtr) this.free(sinPtr);
771
+ return result;
772
+ }
773
+
774
+ /**
775
+ * Analytic needle P-function scan : mirrors tmmNeedleScan() in
776
+ * thinFilmMath.js, reshaping the flat WASM output into the SAME nested
777
+ * structure the synthesis scanners consume.
778
+ * @param {{n:[number,number],d:number}[]} layers used AS-IS (index parity)
779
+ * @param {[number,number][]} candidateNs candidate ñ
780
+ * @param {number[]} intraFracs intra-layer split fractions
781
+ * @returns {{R,T,A,N, gaps:Array, intra:Array}}
782
+ * gaps[pos][ci] = {dR,dT,dA} (pos = 0..N)
783
+ * intra[k][fi] = {frac, perCand:[{dR,dT,dA}]}
784
+ */
785
+ tmmNeedleScan(lambda_nm, theta_deg, polCode, n0, ns, layers, candidateNs, intraFracs = []) {
786
+ const N = layers.length;
787
+ const nCand = candidateNs.length;
788
+ const nFrac = intraFracs.length;
789
+ const nGap = (N + 1) * nCand * 3;
790
+ const nIntra = Math.max(1, N * nFrac * nCand * 3);
791
+
792
+ const layPtr = this._alloc(Math.max(1, 3 * N));
793
+ const candPtr = this._alloc(Math.max(1, 2 * nCand));
794
+ const fracPtr = this._alloc(Math.max(1, nFrac));
795
+ const basePtr = this._alloc(3);
796
+ const gapPtr = this._alloc(Math.max(1, nGap));
797
+ const intraPtr = this._alloc(nIntra);
798
+
799
+ const lay = this._view(layPtr, Math.max(1, 3 * N));
800
+ for (let i = 0; i < N; i++) {
801
+ lay[3 * i + 0] = layers[i].n[0];
802
+ lay[3 * i + 1] = layers[i].n[1];
803
+ lay[3 * i + 2] = layers[i].d;
804
+ }
805
+ const cand = this._view(candPtr, Math.max(1, 2 * nCand));
806
+ for (let c = 0; c < nCand; c++) { cand[2 * c] = candidateNs[c][0]; cand[2 * c + 1] = candidateNs[c][1]; }
807
+ const frac = this._view(fracPtr, Math.max(1, nFrac));
808
+ for (let i = 0; i < nFrac; i++) frac[i] = intraFracs[i];
809
+
810
+ this._tmm_needle_scan(lambda_nm, theta_deg, polCode | 0,
811
+ n0[0], n0[1], ns[0], ns[1], layPtr, N, candPtr, nCand, fracPtr, nFrac,
812
+ basePtr, gapPtr, intraPtr);
813
+
814
+ // Copy outputs out before freeing, reshaping to the JS nested layout.
815
+ const base = this._view(basePtr, 3);
816
+ const R = base[0], T = base[1], A = base[2];
817
+ const gapV = this._view(gapPtr, Math.max(1, nGap));
818
+ const gaps = new Array(N + 1);
819
+ for (let pos = 0; pos <= N; pos++) {
820
+ const row = new Array(nCand);
821
+ for (let c = 0; c < nCand; c++) {
822
+ const o = (pos * nCand + c) * 3;
823
+ row[c] = { dR: gapV[o], dT: gapV[o + 1], dA: gapV[o + 2] };
824
+ }
825
+ gaps[pos] = row;
826
+ }
827
+ const intra = [];
828
+ if (nFrac > 0) {
829
+ const intraV = this._view(intraPtr, nIntra);
830
+ for (let k = 0; k < N; k++) {
831
+ const rowK = [];
832
+ for (let fi = 0; fi < nFrac; fi++) {
833
+ const perCand = new Array(nCand);
834
+ for (let c = 0; c < nCand; c++) {
835
+ const o = ((k * nFrac + fi) * nCand + c) * 3;
836
+ perCand[c] = { dR: intraV[o], dT: intraV[o + 1], dA: intraV[o + 2] };
837
+ }
838
+ rowK.push({ frac: intraFracs[fi], perCand });
839
+ }
840
+ intra.push(rowK);
841
+ }
842
+ }
843
+
844
+ for (const p of [layPtr, candPtr, fracPtr, basePtr, gapPtr, intraPtr]) this.free(p);
845
+ return { R, T, A, gaps, intra, N };
846
+ }
847
+ }
848
+
849
+ /** Instantiate from raw bytes (ArrayBuffer / Uint8Array). Sets the singleton. */
850
+ export async function instantiateTmmWasm(bytes) {
851
+ const { instance } = await WebAssembly.instantiate(
852
+ bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), wasmImports());
853
+ _instance = new TmmWasmInstance(instance);
854
+ return _instance;
855
+ }
856
+
857
+ /** Renderer/Node helper: fetch the `.wasm` at `url` and instantiate it. */
858
+ export function initTmmWasmFromUrl(url) {
859
+ if (_initPromise) return _initPromise;
860
+ _initPromise = (async () => {
861
+ try {
862
+ const resp = await fetch(url);
863
+ if (!resp.ok) throw new Error(`fetch ${url} → ${resp.status}`);
864
+ const buf = await resp.arrayBuffer();
865
+ await instantiateTmmWasm(buf);
866
+ return true;
867
+ } catch (e) {
868
+ // Not built yet / not found : silent fallback to JS.
869
+ _instance = null;
870
+ return false;
871
+ }
872
+ })();
873
+ return _initPromise;
874
+ }
875
+
876
+ export function setTmmWasmEnabled(on) { _enabled = !!on; }
877
+ /** TEST-ONLY: inject a TmmWasmInstance (or null) directly, bypassing the .wasm
878
+ * fetch/instantiate, so the integration seam can be exercised with a mock. */
879
+ export function __setTmmWasmInstanceForTest(inst) { _instance = inst; }
880
+
881
+ // ── Cross-thread plumbing ────────────────────────────────────────────────────
882
+ // The renderer (main thread) loads the .wasm bytes once via IPC, instantiates
883
+ // its own module, and BROADCASTS the same bytes to each pool/worker (workers
884
+ // can't fetch a file:// asset under contextIsolation). Each worker instantiates
885
+ // its OWN module (no shared memory) and enables the flag.
886
+
887
+ let _workerBytes = null; // main-side: raw bytes to hand to workers
888
+ let _workerInitPromise = null; // worker-side: in-flight instantiation
889
+
890
+ /**
891
+ * MAIN THREAD bootstrap. Store the bytes for worker broadcast and, if the user
892
+ * enabled the feature, instantiate the main-thread module and flip the flag.
893
+ * Safe to call once at startup; failures fall back to JS silently.
894
+ */
895
+ export async function initTmmWasmMainThread(bytes, enabled) {
896
+ if (bytes) _workerBytes = bytes; // remember for workers + later toggles
897
+ if (!enabled) { _enabled = false; return false; } // toggle off → JS everywhere
898
+ if (!_workerBytes) return false; // artifact never loaded
899
+ if (!_instance) { // instantiate once; reuse on re-toggle
900
+ try { await instantiateTmmWasm(_workerBytes); }
901
+ catch (_) { _instance = null; _enabled = false; return false; }
902
+ }
903
+ _enabled = true;
904
+ return true;
905
+ }
906
+
907
+ /** MAIN THREAD: bytes to ship to a worker : only when the feature is active. */
908
+ export function getTmmWasmBytesForWorker() {
909
+ return (_enabled && _workerBytes) ? _workerBytes : null;
910
+ }
911
+
912
+ /**
913
+ * WORKER side: kick off one-time instantiation from broadcast bytes and enable
914
+ * the flag in this worker. Idempotent; no-op without bytes or once instantiated.
915
+ */
916
+ export function noteTmmWasmBytes(bytes) {
917
+ if (!bytes || _instance || _workerInitPromise) return;
918
+ _workerInitPromise = instantiateTmmWasm(bytes)
919
+ .then(() => { _enabled = true; return true; })
920
+ .catch(() => { _instance = null; _enabled = false; return false; });
921
+ }
922
+
923
+ /** WORKER side: await any in-flight instantiation before processing a job. */
924
+ export function awaitTmmWasmReady() {
925
+ return _workerInitPromise || Promise.resolve(_instance !== null);
926
+ }
927
+ export function isTmmWasmEnabled() { return _enabled; }
928
+ export function isTmmWasmReady() { return _instance !== null; }
929
+ /** Active iff the feature flag is on AND a module is instantiated. */
930
+ export function tmmWasmActive() { return _enabled && _instance !== null; }
931
+ export function getTmmWasm() { return _instance; }