tmmcore 0.1.0 → 0.2.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.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * taylorJet.js : third-order truncated Taylor arithmetic for complex functions.
3
+ *
4
+ * A jet stores the coefficients of a truncated power series:
5
+ *
6
+ * [ f, f', f''/2!, f'''/3! ]
7
+ *
8
+ * each entry a complex `[re, im]` pair. Every operation below is ordinary power-
9
+ * series algebra, so composing them differentiates a function exactly, with no
10
+ * finite differences and no step size to choose. Feed a jet through the same
11
+ * code that computes a value and the derivatives come out alongside it.
12
+ *
13
+ * This is what makes the phase-dispersion kernel possible: carry the
14
+ * characteristic matrix in jets of angular frequency and the reflection
15
+ * coefficient emerges with the three derivatives that group delay, GDD and TOD
16
+ * are built from.
17
+ *
18
+ * The differentiation variable is whatever the caller chose. In `phase.js` it is
19
+ * angular frequency, so a jet of the refractive index means
20
+ * `[ñ, dñ/dω, (d²ñ/dω²)/2, (d³ñ/dω³)/6]`. Use `jetFromDerivatives` to build one
21
+ * from plain derivatives without doing the factorials by hand, and
22
+ * `jetDerivatives` to read them back.
23
+ *
24
+ * All arithmetic is double precision.
25
+ */
26
+
27
+ /** Highest derivative carried. Jets are arrays of `JET_ORDER + 1` entries. */
28
+ export const JET_ORDER = 3;
29
+
30
+ const zero = () => [0, 0];
31
+ const addComplex = (a, b) => [a[0] + b[0], a[1] + b[1]];
32
+ const subComplex = (a, b) => [a[0] - b[0], a[1] - b[1]];
33
+ const scaleComplex = (a, scalar) => [a[0] * scalar, a[1] * scalar];
34
+ const multiplyComplex = (a, b) => [
35
+ a[0] * b[0] - a[1] * b[1],
36
+ a[0] * b[1] + a[1] * b[0],
37
+ ];
38
+ const divideComplex = (a, b) => {
39
+ const denominator = b[0] * b[0] + b[1] * b[1];
40
+ return [
41
+ (a[0] * b[0] + a[1] * b[1]) / denominator,
42
+ (a[1] * b[0] - a[0] * b[1]) / denominator,
43
+ ];
44
+ };
45
+ const sqrtComplex = (value) => {
46
+ const magnitudeRoot = Math.sqrt(Math.hypot(value[0], value[1]));
47
+ const angle = Math.atan2(value[1], value[0]) / 2;
48
+ return [magnitudeRoot * Math.cos(angle), magnitudeRoot * Math.sin(angle)];
49
+ };
50
+
51
+ /** A constant: value with all derivatives zero. */
52
+ export function jetConstant(value, imaginary = 0) {
53
+ return [[value, imaginary], zero(), zero(), zero()];
54
+ }
55
+
56
+ /**
57
+ * Build a jet from plain derivatives, applying the factorials.
58
+ * Each argument is a number or a complex `[re, im]` pair.
59
+ */
60
+ export function jetFromDerivatives(value, first = 0, second = 0, third = 0) {
61
+ const asComplex = item => Array.isArray(item) ? [...item] : [item, 0];
62
+ return [
63
+ asComplex(value),
64
+ asComplex(first),
65
+ scaleComplex(asComplex(second), 1 / 2),
66
+ scaleComplex(asComplex(third), 1 / 6),
67
+ ];
68
+ }
69
+
70
+ /** Read a jet back as plain derivatives `[f, f', f'', f''']`. */
71
+ export function jetDerivatives(jet) {
72
+ return [jet[0], jet[1], scaleComplex(jet[2], 2), scaleComplex(jet[3], 6)];
73
+ }
74
+
75
+ export function jetAdd(left, right) {
76
+ return left.map((value, index) => addComplex(value, right[index]));
77
+ }
78
+
79
+ export function jetSubtract(left, right) {
80
+ return left.map((value, index) => subComplex(value, right[index]));
81
+ }
82
+
83
+ /** Multiply every order by a real scalar. */
84
+ export function jetScale(jet, scalar) {
85
+ return jet.map(value => scaleComplex(value, scalar));
86
+ }
87
+
88
+ export function jetMultiply(left, right) {
89
+ const result = Array.from({ length: JET_ORDER + 1 }, zero);
90
+ for (let order = 0; order <= JET_ORDER; order++) {
91
+ for (let index = 0; index <= order; index++) {
92
+ result[order] = addComplex(
93
+ result[order],
94
+ multiplyComplex(left[index], right[order - index]),
95
+ );
96
+ }
97
+ }
98
+ return result;
99
+ }
100
+
101
+ export function jetReciprocal(jet) {
102
+ const result = Array.from({ length: JET_ORDER + 1 }, zero);
103
+ result[0] = divideComplex([1, 0], jet[0]);
104
+ for (let order = 1; order <= JET_ORDER; order++) {
105
+ let sum = zero();
106
+ for (let index = 1; index <= order; index++) {
107
+ sum = addComplex(sum, multiplyComplex(jet[index], result[order - index]));
108
+ }
109
+ result[order] = scaleComplex(divideComplex(sum, jet[0]), -1);
110
+ }
111
+ return result;
112
+ }
113
+
114
+ export function jetDivide(numerator, denominator) {
115
+ return jetMultiply(numerator, jetReciprocal(denominator));
116
+ }
117
+
118
+ /** Principal square root, branch matching `csqrt` in tmm.js. */
119
+ export function jetSqrt(jet) {
120
+ const result = Array.from({ length: JET_ORDER + 1 }, zero);
121
+ result[0] = sqrtComplex(jet[0]);
122
+ const twiceRoot = scaleComplex(result[0], 2);
123
+ for (let order = 1; order <= JET_ORDER; order++) {
124
+ let known = zero();
125
+ for (let index = 1; index < order; index++) {
126
+ known = addComplex(known, multiplyComplex(result[index], result[order - index]));
127
+ }
128
+ result[order] = divideComplex(subComplex(jet[order], known), twiceRoot);
129
+ }
130
+ return result;
131
+ }
132
+
133
+ export function jetExp(jet) {
134
+ const result = Array.from({ length: JET_ORDER + 1 }, zero);
135
+ const magnitude = Math.exp(jet[0][0]);
136
+ result[0] = [magnitude * Math.cos(jet[0][1]), magnitude * Math.sin(jet[0][1])];
137
+ for (let order = 1; order <= JET_ORDER; order++) {
138
+ let sum = zero();
139
+ for (let index = 1; index <= order; index++) {
140
+ sum = addComplex(
141
+ sum,
142
+ scaleComplex(multiplyComplex(jet[index], result[order - index]), index),
143
+ );
144
+ }
145
+ result[order] = scaleComplex(sum, 1 / order);
146
+ }
147
+ return result;
148
+ }
149
+
150
+ export function jetLog(jet) {
151
+ const result = Array.from({ length: JET_ORDER + 1 }, zero);
152
+ result[0] = [Math.log(Math.hypot(jet[0][0], jet[0][1])), Math.atan2(jet[0][1], jet[0][0])];
153
+ const derivative = [jet[1], scaleComplex(jet[2], 2), scaleComplex(jet[3], 3), zero()];
154
+ const quotient = jetMultiply(derivative, jetReciprocal(jet));
155
+ for (let order = 1; order <= JET_ORDER; order++) {
156
+ result[order] = scaleComplex(quotient[order - 1], 1 / order);
157
+ }
158
+ return result;
159
+ }
160
+
161
+ /** Real power via exp(p log z); the log branch cut applies. */
162
+ export function jetPower(jet, exponent) {
163
+ return jetExp(jetScale(jetLog(jet), exponent));
164
+ }
165
+
166
+ /** Sine and cosine together, since the recurrences share their terms. */
167
+ export function jetSinCos(jet) {
168
+ const sine = Array.from({ length: JET_ORDER + 1 }, zero);
169
+ const cosine = Array.from({ length: JET_ORDER + 1 }, zero);
170
+ const [real, imaginary] = jet[0];
171
+ sine[0] = [Math.sin(real) * Math.cosh(imaginary), Math.cos(real) * Math.sinh(imaginary)];
172
+ cosine[0] = [Math.cos(real) * Math.cosh(imaginary), -Math.sin(real) * Math.sinh(imaginary)];
173
+ for (let order = 1; order <= JET_ORDER; order++) {
174
+ let sineSum = zero();
175
+ let cosineSum = zero();
176
+ for (let index = 1; index <= order; index++) {
177
+ sineSum = addComplex(
178
+ sineSum,
179
+ scaleComplex(multiplyComplex(jet[index], cosine[order - index]), index),
180
+ );
181
+ cosineSum = addComplex(
182
+ cosineSum,
183
+ scaleComplex(multiplyComplex(jet[index], sine[order - index]), index),
184
+ );
185
+ }
186
+ sine[order] = scaleComplex(sineSum, 1 / order);
187
+ cosine[order] = scaleComplex(cosineSum, -1 / order);
188
+ }
189
+ return { sine, cosine };
190
+ }
191
+
192
+ export const jetSin = jet => jetSinCos(jet).sine;
193
+ export const jetCos = jet => jetSinCos(jet).cosine;
194
+
195
+ /**
196
+ * Chain rule for a scalar function known only through its derivatives.
197
+ *
198
+ * Given f(x0) and [f'(x0), f''(x0), f'''(x0)], and a jet for x, return the jet
199
+ * for f(x). This is the bridge from a dispersion formula differentiated by hand
200
+ * or by a symbolic tool into the jet arithmetic here.
201
+ */
202
+ export function jetCompose(value, derivatives, xJet) {
203
+ const displacement = xJet.map((coefficient, index) => index === 0 ? zero() : coefficient);
204
+ const first = jetScale(displacement, derivatives[0] ?? 0);
205
+ const second = jetScale(jetMultiply(displacement, displacement), (derivatives[1] ?? 0) / 2);
206
+ const third = jetScale(
207
+ jetMultiply(jetMultiply(displacement, displacement), displacement),
208
+ (derivatives[2] ?? 0) / 6,
209
+ );
210
+ return jetAdd(jetAdd(jetConstant(value), first), jetAdd(second, third));
211
+ }
212
+
213
+ /**
214
+ * The jet of wavelength as a function of angular frequency, λ(ω) = 2πc/ω.
215
+ *
216
+ * Needs no value for c: with λ and ω both given, every derivative follows from
217
+ * λ' = -λ/ω. Units are therefore the caller's own, and the wavelength unit of
218
+ * the result is whatever `lambda` was given in.
219
+ */
220
+ export function wavelengthOmegaJet(lambda, omega) {
221
+ // Written as repeated multiplication rather than `omega ** 3` so the C
222
+ // kernel can produce the identical double.
223
+ return [
224
+ [lambda, 0],
225
+ [-lambda / omega, 0],
226
+ [lambda / (omega * omega), 0],
227
+ [-lambda / (omega * omega * omega), 0],
228
+ ];
229
+ }
230
+
231
+ /** Combine a real-valued jet and an imaginary-valued jet into one complex jet. */
232
+ export function jetWithImaginaryPart(realJet, imaginaryJet) {
233
+ return realJet.map((coefficient, index) => [coefficient[0], imaginaryJet[index][0]]);
234
+ }
235
+
236
+ /** Floor the value at `minimum`, flattening the jet to a constant when it bites. */
237
+ export function jetClampRealMinimum(jet, minimum) {
238
+ return jet[0][0] >= minimum ? jet : jetConstant(minimum);
239
+ }
240
+
241
+ /**
242
+ * Bound the imaginary part, flattening the jet to a constant when it bites.
243
+ *
244
+ * Guards the same overflow `layerMatrix` guards in tmm.js: the phase thickness
245
+ * of a strongly absorbing layer grows without bound and cosh of it overflows.
246
+ * Past the limit the layer is already opaque, so the derivatives are zero to
247
+ * machine precision and dropping them keeps the matrix finite.
248
+ */
249
+ export function jetClampImaginary(jet, limit) {
250
+ if (jet[0][1] > limit) {
251
+ return jet.map((coefficient, index) => [coefficient[0], index === 0 ? limit : 0]);
252
+ }
253
+ if (jet[0][1] < -limit) {
254
+ return jet.map((coefficient, index) => [coefficient[0], index === 0 ? -limit : 0]);
255
+ }
256
+ return jet;
257
+ }
258
+
259
+ export function jetIsFinite(jet) {
260
+ return jet.every(coefficient => coefficient.every(Number.isFinite));
261
+ }
package/src/tmm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Transfer-matrix method for multilayer thin films JavaScript reference
2
+ * Transfer-matrix method for multilayer thin films: JavaScript reference
3
3
  * implementation.
4
4
  *
5
5
  * System model:
@@ -226,7 +226,7 @@ export function tmm(lambda_nm, theta_deg, pol, n0, ns, layers) {
226
226
  // dA/dd = −(dR/dd + dT/dd)
227
227
  // The host layer cancels automatically through Pre/Post (a needle of the
228
228
  // host index at an interior point gives ~0), so no nₐ²−n_host² term is
229
- // needed exactly as in Sullivan's scheme.
229
+ // needed, exactly as in Sullivan's scheme.
230
230
  //
231
231
  // Returns { R, T, A, gaps, intra } where
232
232
  // gaps[pos] = [{dR,dT,dA} per candidate] pos = 0..N
@@ -360,7 +360,7 @@ export function tmmNeedleScan(lambda_nm, theta_deg, pol, n0, ns, layers,
360
360
  // dM_k/dd_k = Q · [[ −sinδ, −i cosδ / η ],
361
361
  // [ −i η cosδ, −sinδ ]]
362
362
  //
363
- // As δ→0 this collapses to [[0,−iQ/η],[−iQη,0]] exactly the needle
363
+ // As δ→0 this collapses to [[0,−iQ/η],[−iQη,0]], exactly the needle
364
364
  // A-matrix in tmmNeedleScan (needleA), i.e. the needle kernel is the δ=0
365
365
  // special case of this; a strong internal-consistency check.
366
366
  //
@@ -440,7 +440,7 @@ export function tmmThicknessJacobian(lambda_nm, theta_deg, pol, n0, ns, layers)
440
440
  // order optimization methods in the synthesis of multilayer coatings," Comp.
441
441
  // Maths. Math. Phys. 33, 1339 (1993)).
442
442
  //
443
- // Derivation (same Abelès matrix calculus as the Jacobian Macleod Eq.
443
+ // Derivation (same Abelès matrix calculus as the Jacobian, Macleod Eq.
444
444
  // 2.111/2.113; pre/post decomposition Sullivan & Dobrowolski 1996):
445
445
  // [B,C] = M₀···M_{N-1}·[1,ηs]; ∂[B,C]/∂dₖ = Pre[k]·(dMₖ)·Post[k+1].
446
446
  // Mixed second partials (i < j, position-ordered):
package/src/tmmWasm.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * tmmWasm.js loader and ergonomic wrappers for the WebAssembly TMM kernel.
2
+ * tmmWasm.js : loader and ergonomic wrappers for the WebAssembly TMM kernel.
3
3
  *
4
4
  * The kernel (`tmm_kernel.c`, built to `tmm_kernel.wasm`) is a line-by-line port
5
- * of the JavaScript TMM in `tmm.js`. This module instantiates it in a browser
6
- * main thread, a Web Worker, or Node and exposes wrappers whose signatures
5
+ * of the JavaScript TMM in `tmm.js`. This module instantiates it : in a browser
6
+ * main thread, a Web Worker, or Node : and exposes wrappers whose signatures
7
7
  * mirror the JS functions.
8
8
  *
9
9
  * Acceleration is opt-in and falls back to JavaScript: if the `.wasm` is
@@ -11,16 +11,44 @@
11
11
  * return `null` and callers use the JS path. Results are identical either way
12
12
  * to float64 round-off.
13
13
  *
14
- * Instances are not shared across threads there is no shared memory, so each
14
+ * Instances are not shared across threads : there is no shared memory, so each
15
15
  * context instantiates its own from the same bytes. Use `instantiateTmmWasm()`
16
16
  * where you already hold the bytes (a worker receives them in its init message)
17
17
  * and `initTmmWasmFromUrl()` where the artifact is fetchable.
18
18
  */
19
19
 
20
+ import { omegaFromLambdaNm } from './phase.js';
21
+
20
22
  let _instance = null; // TmmWasmInstance | null
21
23
  let _enabled = false; // feature flag (default OFF)
22
24
  let _initPromise = null; // de-dupe concurrent init
23
25
 
26
+ // ── Jet marshalling ──────────────────────────────────────────────────────────
27
+ // A jet crosses the boundary as 8 doubles, [re, im] per order.
28
+
29
+ function writeJet(buf, offset, jet) {
30
+ for (let i = 0; i < 4; i++) {
31
+ buf[offset + 2 * i] = jet[i][0];
32
+ buf[offset + 2 * i + 1] = jet[i][1];
33
+ }
34
+ }
35
+
36
+ // The kernel writes NaN where the coefficient is exactly zero and the phase is
37
+ // undefined; the JS reference returns null there, so agree with it.
38
+ function readPhase(buf, offset) {
39
+ const magnitudeSquared = buf[offset + 4];
40
+ if (Number.isNaN(magnitudeSquared)) return null;
41
+ const phaseRad = buf[offset];
42
+ return {
43
+ phaseRad,
44
+ phaseDeg: phaseRad * 180 / Math.PI,
45
+ gd: buf[offset + 1],
46
+ gdd: buf[offset + 2],
47
+ tod: buf[offset + 3],
48
+ magnitudeSquared,
49
+ };
50
+ }
51
+
24
52
  // Permissive imports: a STANDALONE_WASM build of pure-math C usually needs no
25
53
  // imports, but ALLOW_MEMORY_GROWTH may emit `emscripten_notify_memory_growth`,
26
54
  // and some toolchains emit WASI stubs. Cover them so instantiation never throws.
@@ -49,6 +77,11 @@ export class TmmWasmInstance {
49
77
  // Optional (added later for SQP/Newton accel): a .wasm built before the
50
78
  // Hessian kernel existed simply lacks it → callers fall back to JS.
51
79
  this._tmm_hessian = ex.tmm_hessian || ex._tmm_hessian || null;
80
+ // Optional, same reason: the phase-dispersion kernel arrived after the
81
+ // spectral one, so an older artifact lacks these three.
82
+ this._tmm_phase_one = ex.tmm_phase_one || ex._tmm_phase_one || null;
83
+ this._tmm_phase_spectrum = ex.tmm_phase_spectrum || ex._tmm_phase_spectrum || null;
84
+ this._tmm_phase_jacobian = ex.tmm_phase_jacobian || ex._tmm_phase_jacobian || null;
52
85
  const missingExports = !this.malloc || !this.free || !this._tmm_one ||
53
86
  !this._tmm_spectrum || !this._tmm_jacobian || !this._tmm_needle_scan;
54
87
  if (missingExports) {
@@ -63,7 +96,7 @@ export class TmmWasmInstance {
63
96
  if (!ptr) throw new Error('tmmWasm: malloc failed');
64
97
  return ptr;
65
98
  }
66
- // Fresh view memory.buffer is detached after any growth, so re-create
99
+ // Fresh view : memory.buffer is detached after any growth, so re-create
67
100
  // views AFTER all mallocs for a call are done.
68
101
  _view(ptr, nDoubles) {
69
102
  return new Float64Array(this.memory.buffer, ptr, nDoubles);
@@ -84,7 +117,7 @@ export class TmmWasmInstance {
84
117
  }
85
118
 
86
119
  /**
87
- * Single (λ, θ, pol) mirrors tmm() in thinFilmMath.js.
120
+ * Single (λ, θ, pol) : mirrors tmm() in thinFilmMath.js.
88
121
  * @returns {{R:number,T:number,A:number}}
89
122
  */
90
123
  tmmOne(lambda_nm, theta_deg, polCode /* 0=s,1=p */, n0, ns, layers) {
@@ -104,7 +137,7 @@ export class TmmWasmInstance {
104
137
  }
105
138
 
106
139
  /**
107
- * Batched spectrum over a λ grid for BOTH polarizations the boundary-
140
+ * Batched spectrum over a λ grid for BOTH polarizations : the boundary-
108
141
  * amortizing path behind evaluateSpectrum().
109
142
  * @param {number[]} lambdas
110
143
  * @param {[number,number][]} n0List incident ñ per λ
@@ -160,7 +193,7 @@ export class TmmWasmInstance {
160
193
  }
161
194
 
162
195
  /**
163
- * Analytic thickness Jacobian for one (λ, θ, pol) mirrors
196
+ * Analytic thickness Jacobian for one (λ, θ, pol) : mirrors
164
197
  * tmmThicknessJacobian(). layers used AS-IS (index parity).
165
198
  * @returns {{R,T,A, dRdd:Float64Array, dTdd, dAdd, N}}
166
199
  */
@@ -182,7 +215,7 @@ export class TmmWasmInstance {
182
215
  n0[0], n0[1], ns[0], ns[1], P(oLay), N, P(oDR), P(oDT), P(oDA), P(oBase));
183
216
  // Re-create the view AFTER the kernel call (like tmmSpectrum): under
184
217
  // ALLOW_MEMORY_GROWTH the kernel may grow wasm memory, which detaches the
185
- // ArrayBuffer `buf` was created over reading the stale `buf` then yields
218
+ // ArrayBuffer `buf` was created over : reading the stale `buf` then yields
186
219
  // garbage / throws. `out` is a fresh view over the current buffer.
187
220
  const out = this._view(ptr, need);
188
221
  return {
@@ -197,7 +230,7 @@ export class TmmWasmInstance {
197
230
  hasHessian() { return !!this._tmm_hessian; }
198
231
 
199
232
  /**
200
- * Analytic thickness Hessian for one (λ, θ, pol) mirrors
233
+ * Analytic thickness Hessian for one (λ, θ, pol) : mirrors
201
234
  * tmmThicknessHessian(). Returns first AND second derivatives; the N×N
202
235
  * second-derivative blocks are reshaped into nested arrays (one Float64Array
203
236
  * row per layer, FULL symmetric) so the shape matches the JS oracle exactly.
@@ -238,8 +271,169 @@ export class TmmWasmInstance {
238
271
  };
239
272
  }
240
273
 
274
+ /** True if the loaded module carries the phase-dispersion kernel. */
275
+ hasPhase() { return !!this._tmm_phase_one; }
276
+
277
+ /**
278
+ * Phase, group delay, GDD and TOD at one wavelength : mirrors
279
+ * tmmPhaseDispersion() in phase.js.
280
+ *
281
+ * @param {number[][]} n0Jet incident-medium index jet, 4 × [re, im]
282
+ * @param {number[][]} nsJet substrate index jet
283
+ * @param {{nJet:number[][], d:number}[]} layers
284
+ * @param {{omega?:number, sinTheta0Jet?:number[][]}} [options]
285
+ * @returns {{r: object|null, t: object|null}}
286
+ */
287
+ tmmPhaseOne(lambda_nm, theta_deg, polCode, n0Jet, nsJet, layers, options = {}) {
288
+ const N = layers.length;
289
+ const omega = options.omega ?? omegaFromLambdaNm(lambda_nm);
290
+ const sinJet = options.sinTheta0Jet || null;
291
+ // arena: layerJets[8N] | thick[N] | n0[8] | ns[8] | sin[8] | out[10]
292
+ const oLay = 0, oThick = 8 * N, oN0 = oThick + N, oNs = oN0 + 8,
293
+ oSin = oNs + 8, oOut = oSin + 8;
294
+ const need = oOut + 10;
295
+ const ptr = this._scratch(need);
296
+ const buf = this._view(ptr, need);
297
+ for (let i = 0; i < N; i++) {
298
+ writeJet(buf, oLay + 8 * i, layers[i].nJet);
299
+ buf[oThick + i] = layers[i].d;
300
+ }
301
+ writeJet(buf, oN0, n0Jet);
302
+ writeJet(buf, oNs, nsJet);
303
+ if (sinJet) writeJet(buf, oSin, sinJet);
304
+ const P = (off) => ptr + off * 8;
305
+ this._tmm_phase_one(lambda_nm, omega, theta_deg, polCode | 0,
306
+ P(oN0), P(oNs), P(oLay), P(oThick), N, sinJet ? P(oSin) : 0, P(oOut));
307
+ const out = this._view(ptr, need);
308
+ return { r: readPhase(out, oOut), t: readPhase(out, oOut + 5) };
309
+ }
310
+
311
+ /**
312
+ * Batched phase dispersion over a λ grid : the boundary-amortizing path.
313
+ *
314
+ * Unlike `tmmSpectrum` this takes one polarization, because the kernel is an
315
+ * order of magnitude dearer per sample and callers at normal incidence would
316
+ * otherwise pay twice for the same numbers.
317
+ *
318
+ * @param {number[]} lambdas
319
+ * @param {number[][][]} n0Jets index jet per λ
320
+ * @param {number[][][]} nsJets index jet per λ
321
+ * @param {number[][][][]} layerJets [layer][λ] = index jet
322
+ * @param {number[]} thick layer thicknesses (nm), length N
323
+ * @param {{omegas?:number[], sinJets?:number[][][]}} [options]
324
+ * @returns {{r: object, t: object}} each `{phaseRad, gd, gdd, tod,
325
+ * magnitudeSquared}` of Float64Array(nLam). Failed samples hold NaN.
326
+ */
327
+ tmmPhaseSpectrum(lambdas, n0Jets, nsJets, layerJets, thick, theta_deg, polCode, options = {}) {
328
+ const nLam = lambdas.length;
329
+ const N = thick.length;
330
+ const omegas = options.omegas
331
+ || lambdas.map(lambda => omegaFromLambdaNm(lambda));
332
+ const sinJets = options.sinJets || null;
333
+
334
+ const lamPtr = this._alloc(nLam);
335
+ const omPtr = this._alloc(nLam);
336
+ const n0Ptr = this._alloc(8 * nLam);
337
+ const nsPtr = this._alloc(8 * nLam);
338
+ const matPtr = this._alloc(Math.max(1, 8 * N * nLam));
339
+ const thPtr = this._alloc(Math.max(1, N));
340
+ const sinPtr = sinJets ? this._alloc(8 * nLam) : 0;
341
+ const outPtr = this._alloc(10 * nLam);
342
+
343
+ // Views created after all mallocs (the buffer may have grown/detached).
344
+ const lam = this._view(lamPtr, nLam);
345
+ const om = this._view(omPtr, nLam);
346
+ const n0v = this._view(n0Ptr, 8 * nLam);
347
+ const nsv = this._view(nsPtr, 8 * nLam);
348
+ const matv = this._view(matPtr, Math.max(1, 8 * N * nLam));
349
+ const thv = this._view(thPtr, Math.max(1, N));
350
+ const sinv = sinJets ? this._view(sinPtr, 8 * nLam) : null;
351
+ for (let i = 0; i < nLam; i++) {
352
+ lam[i] = lambdas[i];
353
+ om[i] = omegas[i];
354
+ writeJet(n0v, 8 * i, n0Jets[i]);
355
+ writeJet(nsv, 8 * i, nsJets[i]);
356
+ if (sinv) writeJet(sinv, 8 * i, sinJets[i]);
357
+ }
358
+ for (let k = 0; k < N; k++) {
359
+ thv[k] = thick[k];
360
+ const row = layerJets[k];
361
+ const base = k * nLam * 8;
362
+ for (let i = 0; i < nLam; i++) writeJet(matv, base + 8 * i, row[i]);
363
+ }
364
+
365
+ this._tmm_phase_spectrum(lamPtr, omPtr, nLam, n0Ptr, nsPtr, matPtr, thPtr, N,
366
+ theta_deg, polCode | 0, sinPtr, outPtr);
367
+
368
+ // De-interleave into one array per quantity before freeing.
369
+ const out = this._view(outPtr, 10 * nLam);
370
+ const side = (base) => {
371
+ const q = {
372
+ phaseRad: new Float64Array(nLam), gd: new Float64Array(nLam),
373
+ gdd: new Float64Array(nLam), tod: new Float64Array(nLam),
374
+ magnitudeSquared: new Float64Array(nLam),
375
+ };
376
+ const keys = ['phaseRad', 'gd', 'gdd', 'tod', 'magnitudeSquared'];
377
+ for (let i = 0; i < nLam; i++) {
378
+ for (let j = 0; j < 5; j++) q[keys[j]][i] = out[10 * i + base + j];
379
+ }
380
+ return q;
381
+ };
382
+ const result = { r: side(0), t: side(5) };
383
+ for (const p of [lamPtr, omPtr, n0Ptr, nsPtr, matPtr, thPtr, outPtr]) this.free(p);
384
+ if (sinPtr) this.free(sinPtr);
385
+ return result;
386
+ }
387
+
388
+ /**
389
+ * Phase dispersion plus exact thickness derivatives : mirrors
390
+ * tmmPhaseThicknessJacobian(). Layers used AS-IS (index parity).
391
+ *
392
+ * @returns {{r, t}} each the phase quantities plus `dPhaseDeg`, `dGd`,
393
+ * `dGdd`, `dTod` as Float64Array(N), or `null` arrays on overflow.
394
+ */
395
+ tmmPhaseJacobian(lambda_nm, theta_deg, polCode, n0Jet, nsJet, layers, options = {}) {
396
+ const N = layers.length;
397
+ const M = Math.max(1, N);
398
+ const omega = options.omega ?? omegaFromLambdaNm(lambda_nm);
399
+ const sinJet = options.sinTheta0Jet || null;
400
+ // arena: layerJets[8N] | thick[N] | n0[8] | ns[8] | sin[8] | out[10] | deriv[8M]
401
+ const oLay = 0, oThick = 8 * N, oN0 = oThick + N, oNs = oN0 + 8,
402
+ oSin = oNs + 8, oOut = oSin + 8, oDeriv = oOut + 10;
403
+ const need = oDeriv + 8 * M;
404
+ const ptr = this._scratch(need);
405
+ const buf = this._view(ptr, need);
406
+ for (let i = 0; i < N; i++) {
407
+ writeJet(buf, oLay + 8 * i, layers[i].nJet);
408
+ buf[oThick + i] = layers[i].d;
409
+ }
410
+ writeJet(buf, oN0, n0Jet);
411
+ writeJet(buf, oNs, nsJet);
412
+ if (sinJet) writeJet(buf, oSin, sinJet);
413
+ const P = (off) => ptr + off * 8;
414
+ this._tmm_phase_jacobian(lambda_nm, omega, theta_deg, polCode | 0,
415
+ P(oN0), P(oNs), P(oLay), P(oThick), N, sinJet ? P(oSin) : 0,
416
+ P(oOut), P(oDeriv));
417
+ const out = this._view(ptr, need);
418
+ const side = (phaseBase, derivBase) => {
419
+ const base = readPhase(out, phaseBase);
420
+ if (!base) return null;
421
+ // The kernel fills the whole block with NaN when the matrix product
422
+ // overflowed and the prefix/suffix decomposition had to be abandoned.
423
+ const overflowed = N > 0 && Number.isNaN(out[oDeriv + derivBase * N]);
424
+ const take = (q) => overflowed
425
+ ? null
426
+ : out.slice(oDeriv + (derivBase + q) * N, oDeriv + (derivBase + q) * N + N);
427
+ return {
428
+ ...base,
429
+ dPhaseDeg: take(0), dGd: take(1), dGdd: take(2), dTod: take(3),
430
+ };
431
+ };
432
+ return { r: side(oOut, 0), t: side(oOut + 5, 4) };
433
+ }
434
+
241
435
  /**
242
- * Analytic needle P-function scan mirrors tmmNeedleScan() in
436
+ * Analytic needle P-function scan : mirrors tmmNeedleScan() in
243
437
  * thinFilmMath.js, reshaping the flat WASM output into the SAME nested
244
438
  * structure the synthesis scanners consume.
245
439
  * @param {{n:[number,number],d:number}[]} layers used AS-IS (index parity)
@@ -332,7 +526,7 @@ export function initTmmWasmFromUrl(url) {
332
526
  await instantiateTmmWasm(buf);
333
527
  return true;
334
528
  } catch (e) {
335
- // Not built yet / not found silent fallback to JS.
529
+ // Not built yet / not found : silent fallback to JS.
336
530
  _instance = null;
337
531
  return false;
338
532
  }
@@ -371,7 +565,7 @@ export async function initTmmWasmMainThread(bytes, enabled) {
371
565
  return true;
372
566
  }
373
567
 
374
- /** MAIN THREAD: bytes to ship to a worker only when the feature is active. */
568
+ /** MAIN THREAD: bytes to ship to a worker : only when the feature is active. */
375
569
  export function getTmmWasmBytesForWorker() {
376
570
  return (_enabled && _workerBytes) ? _workerBytes : null;
377
571
  }