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.
@@ -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):