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/README.md +22 -33
- package/package.json +12 -6
- package/src/build.ps1 +8 -2
- package/src/index.js +19 -2
- package/src/phase.js +522 -0
- package/src/taylorJet.js +261 -0
- package/src/tmm.js +4 -4
- package/src/tmmWasm.js +454 -13
- package/src/tmm_kernel.c +798 -7
- package/src/tmm_kernel.wasm +0 -0
package/src/tmmWasm.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tmmWasm.js
|
|
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
|
|
6
|
-
* main thread, a Web Worker, or Node
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|