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/phase.js
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phase.js : analytic group delay, GDD and TOD of a multilayer.
|
|
3
|
+
*
|
|
4
|
+
* The characteristic matrix is evaluated in third-order Taylor arithmetic with
|
|
5
|
+
* angular frequency as the differentiation variable, so the reflection and
|
|
6
|
+
* transmission coefficients come out with their first three frequency
|
|
7
|
+
* derivatives already attached. The reported quantities are then imaginary parts
|
|
8
|
+
* of logarithmic derivatives of the coefficient:
|
|
9
|
+
*
|
|
10
|
+
* GD = Im(r'/r)
|
|
11
|
+
* GDD = Im(r''/r − (r'/r)²)
|
|
12
|
+
* TOD = Im(r'''/r − 3 r'r''/r² + 2 (r'/r)³)
|
|
13
|
+
*
|
|
14
|
+
* No phase unwrapping and no wavelength finite-difference stencil takes part, so
|
|
15
|
+
* a value at one wavelength is independent of every neighbouring sample. Sample
|
|
16
|
+
* wherever you like; the numbers do not move.
|
|
17
|
+
*
|
|
18
|
+
* Conventions are those of tmm.js: ñ = n + ik, exp(−iωt), off-diagonals carrying
|
|
19
|
+
* −i. This is the complex conjugate of Macleod's convention, under which each
|
|
20
|
+
* reported order is minus the derivative of physical phase, so all three equal
|
|
21
|
+
* derivatives of the raw transfer-matrix phase computed here.
|
|
22
|
+
*
|
|
23
|
+
* Units. The differentiation variable is angular frequency, and the caller
|
|
24
|
+
* chooses its unit by choosing `omega`. GD comes back in that unit's reciprocal,
|
|
25
|
+
* GDD in its square, TOD in its cube. The default `omega` uses `C_NM_PER_FS`
|
|
26
|
+
* with wavelengths in nm, giving fs, fs² and fs³.
|
|
27
|
+
*
|
|
28
|
+
* Refractive indices arrive as jets, `[ñ, dñ/dω, (d²ñ/dω²)/2, (d³ñ/dω³)/6]`.
|
|
29
|
+
* The package deliberately owns no material models; build the jets with the
|
|
30
|
+
* arithmetic in taylorJet.js, from whatever dispersion formula or interpolant
|
|
31
|
+
* you use. See the docs for a worked Sellmeier example.
|
|
32
|
+
*
|
|
33
|
+
* The C kernel shipped alongside is a port of this file and agrees with it to
|
|
34
|
+
* float64 round-off (see tests/).
|
|
35
|
+
*
|
|
36
|
+
* Reference:
|
|
37
|
+
* • Birge & Kärtner, "Analysis of the effects of dispersion on the phase of
|
|
38
|
+
* ultrashort pulses", Appl. Opt. 45, 1478-1483 (2006).
|
|
39
|
+
* https://doi.org/10.1364/AO.45.001478
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
jetAdd,
|
|
44
|
+
jetClampImaginary,
|
|
45
|
+
jetConstant,
|
|
46
|
+
jetDerivatives,
|
|
47
|
+
jetDivide,
|
|
48
|
+
jetMultiply,
|
|
49
|
+
jetScale,
|
|
50
|
+
jetSinCos,
|
|
51
|
+
jetSqrt,
|
|
52
|
+
jetSubtract,
|
|
53
|
+
wavelengthOmegaJet,
|
|
54
|
+
} from './taylorJet.js';
|
|
55
|
+
|
|
56
|
+
/** Speed of light in vacuum, nm/fs. Sets the default time unit to fs. */
|
|
57
|
+
export const C_NM_PER_FS = 299.792458;
|
|
58
|
+
|
|
59
|
+
const MATRIX_RESCALE_THRESHOLD = 1e100;
|
|
60
|
+
const MAX_IMAGINARY_PHASE = 50;
|
|
61
|
+
|
|
62
|
+
/** Angular frequency in rad/fs for a vacuum wavelength in nm. */
|
|
63
|
+
export function omegaFromLambdaNm(lambda_nm) {
|
|
64
|
+
return 2 * Math.PI * C_NM_PER_FS / lambda_nm;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Jet-valued 2×2 matrices ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function identityMatrix() {
|
|
70
|
+
return [
|
|
71
|
+
[jetConstant(1), jetConstant(0)],
|
|
72
|
+
[jetConstant(0), jetConstant(1)],
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function zeroMatrix() {
|
|
77
|
+
return [
|
|
78
|
+
[jetConstant(0), jetConstant(0)],
|
|
79
|
+
[jetConstant(0), jetConstant(0)],
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function matrixMultiply(left, right) {
|
|
84
|
+
return [
|
|
85
|
+
[
|
|
86
|
+
jetAdd(jetMultiply(left[0][0], right[0][0]), jetMultiply(left[0][1], right[1][0])),
|
|
87
|
+
jetAdd(jetMultiply(left[0][0], right[0][1]), jetMultiply(left[0][1], right[1][1])),
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
jetAdd(jetMultiply(left[1][0], right[0][0]), jetMultiply(left[1][1], right[1][0])),
|
|
91
|
+
jetAdd(jetMultiply(left[1][0], right[0][1]), jetMultiply(left[1][1], right[1][1])),
|
|
92
|
+
],
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function matrixMagnitude(matrix) {
|
|
97
|
+
let magnitude = 0;
|
|
98
|
+
for (const row of matrix) {
|
|
99
|
+
for (const jet of row) {
|
|
100
|
+
for (const coefficient of jet) {
|
|
101
|
+
magnitude = Math.max(magnitude, Math.abs(coefficient[0]), Math.abs(coefficient[1]));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return magnitude;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The order-0 matrix controls overflow in the physical coefficient. Once
|
|
109
|
+
// selected, one plain scalar rescales every jet order and cancels from r.
|
|
110
|
+
function rescaleMatrix(matrix, threshold) {
|
|
111
|
+
let scale = 0;
|
|
112
|
+
for (const row of matrix) {
|
|
113
|
+
for (const value of row) {
|
|
114
|
+
scale = Math.max(scale, Math.abs(value[0][0]), Math.abs(value[0][1]));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (scale <= threshold) return 0;
|
|
118
|
+
const inverse = 1 / scale;
|
|
119
|
+
for (const row of matrix) {
|
|
120
|
+
for (let column = 0; column < row.length; column++) {
|
|
121
|
+
row[column] = jetScale(row[column], inverse);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Math.log(scale);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function snellCosine(incidentIndex, incidentSine, layerIndex) {
|
|
128
|
+
const layerSine = jetDivide(jetMultiply(incidentIndex, incidentSine), layerIndex);
|
|
129
|
+
return jetSqrt(jetSubtract(jetConstant(1), jetMultiply(layerSine, layerSine)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function admittance(index, cosine, polarization) {
|
|
133
|
+
return polarization === 's'
|
|
134
|
+
? jetMultiply(index, cosine)
|
|
135
|
+
: jetDivide(index, cosine);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function layerMatrix(index, thickness, wavelength, cosine, polarization) {
|
|
139
|
+
const phase = jetClampImaginary(jetScale(
|
|
140
|
+
jetDivide(jetMultiply(index, cosine), wavelength),
|
|
141
|
+
2 * Math.PI * thickness,
|
|
142
|
+
), MAX_IMAGINARY_PHASE);
|
|
143
|
+
const { sine, cosine: cosinePhase } = jetSinCos(phase);
|
|
144
|
+
const eta = admittance(index, cosine, polarization);
|
|
145
|
+
const minusI = jetConstant(0, -1);
|
|
146
|
+
return [
|
|
147
|
+
[cosinePhase, jetMultiply(minusI, jetDivide(sine, eta))],
|
|
148
|
+
[jetMultiply(minusI, jetMultiply(eta, sine)), cosinePhase],
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function layerMatrixWithThicknessDerivative(index, thickness, wavelength, cosine, polarization) {
|
|
153
|
+
const phasePerUnit = jetScale(
|
|
154
|
+
jetDivide(jetMultiply(index, cosine), wavelength),
|
|
155
|
+
2 * Math.PI,
|
|
156
|
+
);
|
|
157
|
+
const rawPhase = jetScale(phasePerUnit, thickness);
|
|
158
|
+
const phase = jetClampImaginary(rawPhase, MAX_IMAGINARY_PHASE);
|
|
159
|
+
// Where the clamp bit, the layer is opaque and the thickness derivative of
|
|
160
|
+
// the imaginary phase is zero to machine precision.
|
|
161
|
+
const phaseDerivative = rawPhase[0][1] === phase[0][1]
|
|
162
|
+
? phasePerUnit
|
|
163
|
+
: phasePerUnit.map(coefficient => [coefficient[0], 0]);
|
|
164
|
+
const { sine, cosine: cosinePhase } = jetSinCos(phase);
|
|
165
|
+
const sineDerivative = jetMultiply(cosinePhase, phaseDerivative);
|
|
166
|
+
const cosineDerivative = jetScale(jetMultiply(sine, phaseDerivative), -1);
|
|
167
|
+
const eta = admittance(index, cosine, polarization);
|
|
168
|
+
const minusI = jetConstant(0, -1);
|
|
169
|
+
return {
|
|
170
|
+
matrix: [
|
|
171
|
+
[cosinePhase, jetMultiply(minusI, jetDivide(sine, eta))],
|
|
172
|
+
[jetMultiply(minusI, jetMultiply(eta, sine)), cosinePhase],
|
|
173
|
+
],
|
|
174
|
+
thicknessDerivative: [
|
|
175
|
+
[cosineDerivative, jetMultiply(minusI, jetDivide(sineDerivative, eta))],
|
|
176
|
+
[jetMultiply(minusI, jetMultiply(eta, sineDerivative)), cosineDerivative],
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function coefficientJetsFromMatrix(matrix, incidentEta, substrateEta, logScale = 0) {
|
|
182
|
+
const boundaryB = jetAdd(matrix[0][0], jetMultiply(matrix[0][1], substrateEta));
|
|
183
|
+
const boundaryC = jetAdd(matrix[1][0], jetMultiply(matrix[1][1], substrateEta));
|
|
184
|
+
const incidentB = jetMultiply(incidentEta, boundaryB);
|
|
185
|
+
const denominator = jetAdd(incidentB, boundaryC);
|
|
186
|
+
const reflectionNumerator = jetSubtract(incidentB, boundaryC);
|
|
187
|
+
const reflection = jetDivide(reflectionNumerator, denominator);
|
|
188
|
+
let transmission = jetDivide(jetScale(incidentEta, 2), denominator);
|
|
189
|
+
if (logScale !== 0) transmission = jetScale(transmission, Math.exp(-logScale));
|
|
190
|
+
return { reflection, transmission, denominator, reflectionNumerator };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function coefficientThicknessJets(matrixDerivative, coefficientData, incidentEta, substrateEta) {
|
|
194
|
+
const boundaryBDerivative = jetAdd(
|
|
195
|
+
matrixDerivative[0][0],
|
|
196
|
+
jetMultiply(matrixDerivative[0][1], substrateEta),
|
|
197
|
+
);
|
|
198
|
+
const boundaryCDerivative = jetAdd(
|
|
199
|
+
matrixDerivative[1][0],
|
|
200
|
+
jetMultiply(matrixDerivative[1][1], substrateEta),
|
|
201
|
+
);
|
|
202
|
+
const incidentBDerivative = jetMultiply(incidentEta, boundaryBDerivative);
|
|
203
|
+
const denominatorDerivative = jetAdd(incidentBDerivative, boundaryCDerivative);
|
|
204
|
+
const numeratorDerivative = jetSubtract(incidentBDerivative, boundaryCDerivative);
|
|
205
|
+
return {
|
|
206
|
+
reflection: jetDivide(
|
|
207
|
+
jetSubtract(
|
|
208
|
+
numeratorDerivative,
|
|
209
|
+
jetMultiply(coefficientData.reflection, denominatorDerivative),
|
|
210
|
+
),
|
|
211
|
+
coefficientData.denominator,
|
|
212
|
+
),
|
|
213
|
+
transmission: jetScale(
|
|
214
|
+
jetDivide(
|
|
215
|
+
jetMultiply(coefficientData.transmission, denominatorDerivative),
|
|
216
|
+
coefficientData.denominator,
|
|
217
|
+
),
|
|
218
|
+
-1,
|
|
219
|
+
),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Coefficient jets ─────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Complex Taylor jets for r and t at one wavelength.
|
|
227
|
+
*
|
|
228
|
+
* Lower level than `tmmPhaseDispersion`: use this when you want the coefficient
|
|
229
|
+
* jets themselves rather than the phase quantities read off them.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} options
|
|
232
|
+
* @param {number[][]} options.wavelengthJet jet of λ(ω), from `wavelengthOmegaJet`
|
|
233
|
+
* @param {number} options.thetaDeg angle of incidence, degrees
|
|
234
|
+
* @param {'s'|'p'} options.polarization
|
|
235
|
+
* @param {number[][]} options.incidentIndexJet
|
|
236
|
+
* @param {number[][]} options.substrateIndexJet
|
|
237
|
+
* @param {number[][]} [options.incidentSineJet] see `tmmPhaseDispersion`
|
|
238
|
+
* @param {{indexJet:number[][], thicknessNm:number}[]} options.layers
|
|
239
|
+
* @returns {{reflection, transmission, incidentEta, substrateEta}}
|
|
240
|
+
*/
|
|
241
|
+
export function tmmCoefficientJets({
|
|
242
|
+
wavelengthJet,
|
|
243
|
+
thetaDeg,
|
|
244
|
+
polarization,
|
|
245
|
+
incidentIndexJet,
|
|
246
|
+
substrateIndexJet,
|
|
247
|
+
incidentSineJet = null,
|
|
248
|
+
rescaleThreshold = MATRIX_RESCALE_THRESHOLD,
|
|
249
|
+
layers,
|
|
250
|
+
}) {
|
|
251
|
+
const incidentSine = incidentSineJet || jetConstant(Math.sin(thetaDeg * Math.PI / 180));
|
|
252
|
+
const incidentCosine = incidentSineJet
|
|
253
|
+
? jetSqrt(jetSubtract(jetConstant(1), jetMultiply(incidentSine, incidentSine)))
|
|
254
|
+
: jetConstant(Math.cos(thetaDeg * Math.PI / 180));
|
|
255
|
+
const incidentEta = admittance(incidentIndexJet, incidentCosine, polarization);
|
|
256
|
+
const substrateCosine = snellCosine(incidentIndexJet, incidentSine, substrateIndexJet);
|
|
257
|
+
const substrateEta = admittance(substrateIndexJet, substrateCosine, polarization);
|
|
258
|
+
|
|
259
|
+
let matrix = identityMatrix();
|
|
260
|
+
let logScale = 0;
|
|
261
|
+
for (const layer of layers) {
|
|
262
|
+
if (!(layer.thicknessNm > 0)) continue;
|
|
263
|
+
const cosine = snellCosine(incidentIndexJet, incidentSine, layer.indexJet);
|
|
264
|
+
matrix = matrixMultiply(matrix, layerMatrix(
|
|
265
|
+
layer.indexJet,
|
|
266
|
+
layer.thicknessNm,
|
|
267
|
+
wavelengthJet,
|
|
268
|
+
cosine,
|
|
269
|
+
polarization,
|
|
270
|
+
));
|
|
271
|
+
logScale += rescaleMatrix(matrix, rescaleThreshold);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const coefficients = coefficientJetsFromMatrix(matrix, incidentEta, substrateEta, logScale);
|
|
275
|
+
return {
|
|
276
|
+
reflection: coefficients.reflection,
|
|
277
|
+
transmission: coefficients.transmission,
|
|
278
|
+
incidentEta,
|
|
279
|
+
substrateEta,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Coefficient jets together with their exact derivatives with respect to every
|
|
285
|
+
* layer thickness. Frequency remains the Taylor variable, so each thickness
|
|
286
|
+
* derivative is itself a third-order frequency jet.
|
|
287
|
+
*
|
|
288
|
+
* Falls back to `tmmCoefficientJets` and reports `thicknessDerivatives: null`
|
|
289
|
+
* when the matrix product overflows, since the prefix/suffix decomposition
|
|
290
|
+
* cannot carry a rescaling.
|
|
291
|
+
*/
|
|
292
|
+
export function tmmCoefficientThicknessJets(options) {
|
|
293
|
+
const {
|
|
294
|
+
wavelengthJet,
|
|
295
|
+
thetaDeg,
|
|
296
|
+
polarization,
|
|
297
|
+
incidentIndexJet,
|
|
298
|
+
substrateIndexJet,
|
|
299
|
+
incidentSineJet = null,
|
|
300
|
+
rescaleThreshold = MATRIX_RESCALE_THRESHOLD,
|
|
301
|
+
layers,
|
|
302
|
+
} = options;
|
|
303
|
+
const incidentSine = incidentSineJet || jetConstant(Math.sin(thetaDeg * Math.PI / 180));
|
|
304
|
+
const incidentCosine = incidentSineJet
|
|
305
|
+
? jetSqrt(jetSubtract(jetConstant(1), jetMultiply(incidentSine, incidentSine)))
|
|
306
|
+
: jetConstant(Math.cos(thetaDeg * Math.PI / 180));
|
|
307
|
+
const incidentEta = admittance(incidentIndexJet, incidentCosine, polarization);
|
|
308
|
+
const substrateCosine = snellCosine(incidentIndexJet, incidentSine, substrateIndexJet);
|
|
309
|
+
const substrateEta = admittance(substrateIndexJet, substrateCosine, polarization);
|
|
310
|
+
const layerData = layers.map((layer) => {
|
|
311
|
+
// The point evaluator skips values that are not positive. Keep a slot in
|
|
312
|
+
// the Jacobian for the same invalid layer, but give it no optical effect
|
|
313
|
+
// and no derivative so both public paths report the same base result.
|
|
314
|
+
// Zero is different: its nonzero derivative is useful for candidate
|
|
315
|
+
// layers and is why the Jacobian deliberately retains it.
|
|
316
|
+
if (!(layer.thicknessNm >= 0)) {
|
|
317
|
+
return { matrix: identityMatrix(), thicknessDerivative: zeroMatrix() };
|
|
318
|
+
}
|
|
319
|
+
const cosine = snellCosine(incidentIndexJet, incidentSine, layer.indexJet);
|
|
320
|
+
return layerMatrixWithThicknessDerivative(
|
|
321
|
+
layer.indexJet,
|
|
322
|
+
layer.thicknessNm,
|
|
323
|
+
wavelengthJet,
|
|
324
|
+
cosine,
|
|
325
|
+
polarization,
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const prefix = new Array(layerData.length + 1);
|
|
330
|
+
prefix[0] = identityMatrix();
|
|
331
|
+
for (let index = 0; index < layerData.length; index++) {
|
|
332
|
+
prefix[index + 1] = matrixMultiply(prefix[index], layerData[index].matrix);
|
|
333
|
+
if (matrixMagnitude(prefix[index + 1]) > rescaleThreshold) {
|
|
334
|
+
const coefficients = tmmCoefficientJets(options);
|
|
335
|
+
return { ...coefficients, thicknessDerivatives: null };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const suffix = new Array(layerData.length + 1);
|
|
339
|
+
suffix[layerData.length] = identityMatrix();
|
|
340
|
+
for (let index = layerData.length - 1; index >= 0; index--) {
|
|
341
|
+
suffix[index] = matrixMultiply(layerData[index].matrix, suffix[index + 1]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const coefficientData = coefficientJetsFromMatrix(
|
|
345
|
+
prefix[layerData.length],
|
|
346
|
+
incidentEta,
|
|
347
|
+
substrateEta,
|
|
348
|
+
);
|
|
349
|
+
const thicknessDerivatives = layerData.map((layer, index) => {
|
|
350
|
+
const matrixDerivative = matrixMultiply(
|
|
351
|
+
matrixMultiply(prefix[index], layer.thicknessDerivative),
|
|
352
|
+
suffix[index + 1],
|
|
353
|
+
);
|
|
354
|
+
return coefficientThicknessJets(
|
|
355
|
+
matrixDerivative,
|
|
356
|
+
coefficientData,
|
|
357
|
+
incidentEta,
|
|
358
|
+
substrateEta,
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
reflection: coefficientData.reflection,
|
|
363
|
+
transmission: coefficientData.transmission,
|
|
364
|
+
reflectionThickness: thicknessDerivatives.map(value => value.reflection),
|
|
365
|
+
transmissionThickness: thicknessDerivatives.map(value => value.transmission),
|
|
366
|
+
incidentEta,
|
|
367
|
+
substrateEta,
|
|
368
|
+
thicknessDerivatives,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Phase quantities ─────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Read phase, GD, GDD and TOD off a coefficient jet.
|
|
376
|
+
*
|
|
377
|
+
* Returns `null` when the coefficient is exactly zero, where the phase and every
|
|
378
|
+
* derivative of it are undefined.
|
|
379
|
+
*/
|
|
380
|
+
export function coefficientPhaseDispersion(coefficientJet) {
|
|
381
|
+
const [value, first, second, third] = jetDerivatives(coefficientJet);
|
|
382
|
+
const magnitudeSquared = value[0] * value[0] + value[1] * value[1];
|
|
383
|
+
if (magnitudeSquared === 0 || !Number.isFinite(magnitudeSquared)) return null;
|
|
384
|
+
|
|
385
|
+
const valueJet = jetConstant(value[0], value[1]);
|
|
386
|
+
const firstRatio = jetDivide(jetConstant(first[0], first[1]), valueJet)[0];
|
|
387
|
+
const secondRatio = jetDivide(jetConstant(second[0], second[1]), valueJet)[0];
|
|
388
|
+
const thirdRatio = jetDivide(jetConstant(third[0], third[1]), valueJet)[0];
|
|
389
|
+
const squareFirst = [
|
|
390
|
+
firstRatio[0] * firstRatio[0] - firstRatio[1] * firstRatio[1],
|
|
391
|
+
2 * firstRatio[0] * firstRatio[1],
|
|
392
|
+
];
|
|
393
|
+
const firstTimesSecond = [
|
|
394
|
+
firstRatio[0] * secondRatio[0] - firstRatio[1] * secondRatio[1],
|
|
395
|
+
firstRatio[0] * secondRatio[1] + firstRatio[1] * secondRatio[0],
|
|
396
|
+
];
|
|
397
|
+
const cubeFirst = [
|
|
398
|
+
squareFirst[0] * firstRatio[0] - squareFirst[1] * firstRatio[1],
|
|
399
|
+
squareFirst[0] * firstRatio[1] + squareFirst[1] * firstRatio[0],
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
const phaseRad = -Math.atan2(value[1], value[0]);
|
|
403
|
+
return {
|
|
404
|
+
phaseRad,
|
|
405
|
+
phaseDeg: phaseRad * 180 / Math.PI,
|
|
406
|
+
gd: firstRatio[1],
|
|
407
|
+
gdd: secondRatio[1] - squareFirst[1],
|
|
408
|
+
tod: thirdRatio[1] - 3 * firstTimesSecond[1] + 2 * cubeFirst[1],
|
|
409
|
+
magnitudeSquared,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Exact derivatives of the reported phase quantities with respect to every layer
|
|
415
|
+
* thickness, from the coefficient jet and its thickness-derivative jets.
|
|
416
|
+
*
|
|
417
|
+
* Returns `null` when the thickness jets are absent, which is how
|
|
418
|
+
* `tmmCoefficientThicknessJets` reports an overflow fallback.
|
|
419
|
+
*/
|
|
420
|
+
export function coefficientPhaseThicknessDerivatives(coefficientJet, thicknessJets) {
|
|
421
|
+
if (!thicknessJets) return null;
|
|
422
|
+
const result = { phaseDeg: [], gd: [], gdd: [], tod: [] };
|
|
423
|
+
for (const thicknessJet of thicknessJets) {
|
|
424
|
+
const logarithmicDerivative = jetDivide(thicknessJet, coefficientJet);
|
|
425
|
+
const derivatives = jetDerivatives(logarithmicDerivative);
|
|
426
|
+
result.phaseDeg.push(-derivatives[0][1] * 180 / Math.PI);
|
|
427
|
+
result.gd.push(derivatives[1][1]);
|
|
428
|
+
result.gdd.push(derivatives[2][1]);
|
|
429
|
+
result.tod.push(derivatives[3][1]);
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Public entry points ──────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
function prepare(lambda, layers, options) {
|
|
437
|
+
const omega = options.omega ?? omegaFromLambdaNm(lambda);
|
|
438
|
+
return {
|
|
439
|
+
wavelengthJet: wavelengthOmegaJet(lambda, omega),
|
|
440
|
+
incidentSineJet: options.sinTheta0Jet || null,
|
|
441
|
+
layers: layers.map(layer => ({ indexJet: layer.nJet, thicknessNm: layer.d })),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Phase, group delay, GDD and TOD of a multilayer at one wavelength, for both
|
|
447
|
+
* the reflection and the transmission coefficient.
|
|
448
|
+
*
|
|
449
|
+
* @param {number} lambda_nm
|
|
450
|
+
* @param {number} theta_deg angle of incidence, degrees from normal
|
|
451
|
+
* @param {'s'|'p'} pol
|
|
452
|
+
* @param {number[][]} n0Jet incident-medium index jet
|
|
453
|
+
* @param {number[][]} nsJet substrate index jet
|
|
454
|
+
* @param {{nJet:number[][], d:number}[]} layers incident medium first, d in nm
|
|
455
|
+
* @param {object} [options]
|
|
456
|
+
* @param {number} [options.omega] angular frequency; sets the time unit of the
|
|
457
|
+
* result. Defaults to 2πc/λ in rad/fs, giving fs, fs² and fs³.
|
|
458
|
+
* @param {number[][]} [options.sinTheta0Jet] sine of the incident angle as a
|
|
459
|
+
* jet. Give this when the stack is embedded in a dispersive medium and the
|
|
460
|
+
* angle held fixed is the external one, so that the internal angle disperses.
|
|
461
|
+
* Defaults to the constant sin(theta_deg).
|
|
462
|
+
* @returns {{r: object|null, t: object|null}} each
|
|
463
|
+
* `{phaseRad, phaseDeg, gd, gdd, tod, magnitudeSquared}`, or `null` where that
|
|
464
|
+
* coefficient is exactly zero.
|
|
465
|
+
*/
|
|
466
|
+
export function tmmPhaseDispersion(lambda_nm, theta_deg, pol, n0Jet, nsJet, layers, options = {}) {
|
|
467
|
+
const prepared = prepare(lambda_nm, layers, options);
|
|
468
|
+
const coefficients = tmmCoefficientJets({
|
|
469
|
+
wavelengthJet: prepared.wavelengthJet,
|
|
470
|
+
thetaDeg: theta_deg,
|
|
471
|
+
polarization: pol,
|
|
472
|
+
incidentIndexJet: n0Jet,
|
|
473
|
+
substrateIndexJet: nsJet,
|
|
474
|
+
incidentSineJet: prepared.incidentSineJet,
|
|
475
|
+
layers: prepared.layers,
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
r: coefficientPhaseDispersion(coefficients.reflection),
|
|
479
|
+
t: coefficientPhaseDispersion(coefficients.transmission),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The same quantities plus their exact derivatives with respect to every layer
|
|
485
|
+
* thickness, for gradient-based dispersion design.
|
|
486
|
+
*
|
|
487
|
+
* Arguments are those of `tmmPhaseDispersion`. Zero-thickness layers are kept so
|
|
488
|
+
* derivative indices line up with the design array. Negative or non-finite
|
|
489
|
+
* thicknesses are skipped and receive zero derivative entries.
|
|
490
|
+
*
|
|
491
|
+
* @returns {{r, t}} where each side is the phase quantities plus
|
|
492
|
+
* `{dPhaseDeg, dGd, dGdd, dTod}`, arrays of length `layers.length`. The
|
|
493
|
+
* derivative arrays are `null` if the matrix product overflowed.
|
|
494
|
+
*/
|
|
495
|
+
export function tmmPhaseThicknessJacobian(lambda_nm, theta_deg, pol, n0Jet, nsJet, layers, options = {}) {
|
|
496
|
+
const prepared = prepare(lambda_nm, layers, options);
|
|
497
|
+
const coefficients = tmmCoefficientThicknessJets({
|
|
498
|
+
wavelengthJet: prepared.wavelengthJet,
|
|
499
|
+
thetaDeg: theta_deg,
|
|
500
|
+
polarization: pol,
|
|
501
|
+
incidentIndexJet: n0Jet,
|
|
502
|
+
substrateIndexJet: nsJet,
|
|
503
|
+
incidentSineJet: prepared.incidentSineJet,
|
|
504
|
+
layers: prepared.layers,
|
|
505
|
+
});
|
|
506
|
+
const side = (coefficientJet, thicknessJets) => {
|
|
507
|
+
const base = coefficientPhaseDispersion(coefficientJet);
|
|
508
|
+
if (!base) return null;
|
|
509
|
+
const derivatives = coefficientPhaseThicknessDerivatives(coefficientJet, thicknessJets);
|
|
510
|
+
return {
|
|
511
|
+
...base,
|
|
512
|
+
dPhaseDeg: derivatives ? derivatives.phaseDeg : null,
|
|
513
|
+
dGd: derivatives ? derivatives.gd : null,
|
|
514
|
+
dGdd: derivatives ? derivatives.gdd : null,
|
|
515
|
+
dTod: derivatives ? derivatives.tod : null,
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
r: side(coefficients.reflection, coefficients.reflectionThickness),
|
|
520
|
+
t: side(coefficients.transmission, coefficients.transmissionThickness),
|
|
521
|
+
};
|
|
522
|
+
}
|