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.
- package/README.md +22 -33
- package/package.json +11 -5
- package/src/build.ps1 +4 -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 +207 -13
- package/src/tmm_kernel.c +468 -7
- package/src/tmm_kernel.wasm +0 -0
package/src/taylorJet.js
ADDED
|
@@ -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
|
|
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
|
|
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]]
|
|
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
|
|
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
|
|
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.
|
|
@@ -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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|