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/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
+ }