radphen-iadpython 0.7.0__py3-none-any.whl

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.
iadpython/combine.py ADDED
@@ -0,0 +1,620 @@
1
+ """Module for adding layers together.
2
+
3
+ Two types of starting methods are possible.
4
+
5
+ Example::
6
+
7
+ >>> import iadpython as iad
8
+
9
+ >>> # Isotropic finite layer with mismatched slides the hard way.
10
+ >>> s = iad.Sample(a=0.5, b=1, g=0.0, n=1.4, n_above=1.5, n_below=1.6)
11
+ >>> s.quad_pts = 4
12
+ >>> R01, R10, T01, T10 = iad.boundary_matrices(s, top=True)
13
+ >>> R23, R32, T23, T32 = iad.boundary_matrices(s, top=False)
14
+ >>> R12, T12 = iad.simple_layer_matrices(s)
15
+ >>> R02, R20, T02, T20 = iad.add_layers(s, R01, R10, T01, T10, R12, R12, T12, T12)
16
+ >>> rr03, rr30, tt03, tt30 = iad.add_layers(s, R02, R20, T02, T20, R23, R32, T23, T32)
17
+
18
+ """
19
+
20
+ import copy
21
+ import scipy
22
+ import numpy as np
23
+ import iadpython.constants
24
+ import iadpython.start
25
+ import iadpython.fresnel
26
+
27
+ __all__ = (
28
+ "add_layers",
29
+ "add_layers_basic",
30
+ "simple_layer_matrices",
31
+ "add_slide_above",
32
+ "add_slide_below",
33
+ "add_same_slides",
34
+ "mismatched_layer_matrices",
35
+ )
36
+
37
+
38
+ def add_layers_basic(sample, R10, T01, R12, R21, T12, T21):
39
+ """Add two layers together.
40
+
41
+ The basic equations for the adding-doubling sample (neglecting sources) are
42
+
43
+ .. math:: T_{02} = T_{12} (E - R_{10} R_{12})^{-1} T_{01}
44
+
45
+ .. math:: R_{20} = T_{12} (E - R_{10} R_{12})^{-1} R_{10} T_{21} +R_{21}
46
+
47
+ .. math:: T_{20} = T_{10} (E - R_{12} R_{10})^{-1} T_{21}
48
+
49
+ .. math:: R_{02} = T_{10} (E - R_{12} R_{10})^{-1} R_{12} T_{01} +R_{01}
50
+
51
+ Upon examination it is clear that the two sets of equations have
52
+ the same form. These equations assume some of the multiplications are
53
+ star multiplications. Explicitly,
54
+
55
+ .. math:: T_{02} = T_{12} (E - R_{10} C R_{12} )^{-1} T_{01}
56
+
57
+ .. math:: R_{20} = T_{12} (E - R_{10} C R_{12} )^{-1} R_{10} C T_{21} +R_{21}
58
+
59
+ where the diagonal matrices C and E are
60
+
61
+ .. math:: E_{ij}= 1/(2𝜈_i w_i) 𝛿_{ij}
62
+
63
+ .. math:: C_{ij}= 2𝜈_i w_i 𝛿_{ij}
64
+
65
+ Args:
66
+ sample: Sample object
67
+ R10: reflection matrix for light moving upwards 1->0
68
+ T01: transmission array for light moving downwards 1->2
69
+ R12: reflection matrix for light moving downwards 1->2
70
+ R21: reflection matrix for light moving upwards 2->1
71
+ T12: transmission matrix for light moving downwards 1->2
72
+ T21: transmission matrix for light moving upwards 2->1
73
+
74
+ Returns:
75
+ R02, T20
76
+ """
77
+ C = np.diagflat(sample.twonuw)
78
+ E = np.diagflat(1 / sample.twonuw)
79
+
80
+ A = E - R10 @ C @ R12
81
+ B = np.linalg.solve(A.T, T12.T).T
82
+ R20 = B @ R10 @ C @ T21 + R21
83
+ T02 = B @ T01
84
+ return R20, T02
85
+
86
+
87
+ def add_layers(sample, R01, R10, T01, T10, R12, R21, T12, T21):
88
+ """Add two layers together.
89
+
90
+ Use this when the combined system is asymmetric R02!=R20 and T02!=T20.
91
+ """
92
+ R20, T02 = add_layers_basic(sample, R10, T01, R12, R21, T12, T21)
93
+ R02, T20 = add_layers_basic(sample, R12, T21, R10, R01, T10, T01)
94
+ return R02, R20, T02, T20
95
+
96
+
97
+ def double_until(sample, r_start, t_start, b_start, b_end):
98
+ """Double until proper thickness is reached."""
99
+ r = r_start
100
+ t = t_start
101
+ if b_end == 0 or b_end <= b_start:
102
+ return r, t
103
+
104
+ if b_end > iadpython.AD_MAX_THICKNESS:
105
+ old_utu = 100
106
+ utu = 10
107
+ while abs(utu - old_utu) > 1e-6:
108
+ old_utu = utu
109
+ r, t = add_layers_basic(sample, r, t, r, r, t, t)
110
+ _, _, _, utu = sample.UX1_and_UXU(r, t)
111
+ return r, t
112
+
113
+ while abs(b_end - b_start) > 0.00001 and b_end > b_start:
114
+ r, t = add_layers_basic(sample, r, t, r, r, t, t)
115
+ b_start *= 2
116
+ return r, t
117
+
118
+
119
+ def simple_single_layer_matrices(sample):
120
+ """Create R and T matrices for single layer without boundaries."""
121
+ # avoid b=0 calculation which leads to singular matrices
122
+ if sample.b <= 0:
123
+ sample.b = 1e-9
124
+ r_start, t_start = iadpython.start.thinnest_layer(sample)
125
+ b_start = sample.b_thinnest
126
+ b_end = sample.b_delta_M()
127
+ r, t = double_until(sample, r_start, t_start, b_start, b_end)
128
+ return r, t
129
+
130
+
131
+ def simple_layer_matrices(sample):
132
+ """Create R and T matrices for a sample without boundaries.
133
+
134
+ Scalar ``a``/``b``/``g`` solve a single homogeneous layer. Array values
135
+ are only accepted from the internal layered convention (a materialized
136
+ ``Sample(layers=[...])``, marked by ``sample._layer_pf``); the old public
137
+ form where flat arrays meant layers has been removed.
138
+ """
139
+ if np.isscalar(sample.a) and np.isscalar(sample.b) and np.isscalar(sample.g):
140
+ return simple_single_layer_matrices(sample)
141
+
142
+ if getattr(sample, "_layer_pf", None) is None:
143
+ raise RuntimeError(
144
+ "array-valued a/b/g now mean wavelengths, not layers; build a "
145
+ "multilayer sample with iadpython.Sample(layers="
146
+ "[iadpython.Layer(...), ...])"
147
+ )
148
+
149
+ return _stack_layer_matrices(sample)
150
+
151
+
152
+ def _stack_layer_matrices(sample):
153
+ """R and T matrices for a matched-index multilayer stack (internal).
154
+
155
+ ``sample`` follows the internal layered convention: ``a``/``b``/``g`` are
156
+ per-layer vectors and ``sample._layer_pf`` holds each layer's phase
157
+ function as ``(pf_type, one_column_pf_data_or_None)``. Every layer may
158
+ therefore use its own Henyey-Greenstein anisotropy or tabulated phase
159
+ function.
160
+ """
161
+ n_layers = len(sample._layer_pf)
162
+ a, b, g = _broadcast_layer_params(sample, n_layers)
163
+
164
+ s = copy.deepcopy(sample)
165
+ s._layer_pf = None # each sub-sample is one homogeneous layer
166
+
167
+ r, t = None, None
168
+ for i in range(n_layers):
169
+ s.a = float(a[i])
170
+ s.b = float(b[i])
171
+ s.g = float(g[i])
172
+ s.pf_type, s.pf_data = sample._layer_pf[i]
173
+
174
+ # Invalidate unconditionally: the g/pf_data setters skip invalidation
175
+ # when a value is unchanged, so an HG layer following a TABULATED
176
+ # layer with the same g could otherwise reuse a stale hp/hm.
177
+ s.hp = None
178
+ s.hm = None
179
+ s.b_thinnest = None
180
+
181
+ ri, ti = simple_single_layer_matrices(s)
182
+ if i == 0:
183
+ r, t = ri, ti
184
+ else:
185
+ r, t = add_layers_basic(s, r, t, ri, ri, ti, ti)
186
+
187
+ return r, t
188
+
189
+
190
+ def _add_boundary_config_a(sample, R12, R21, T12, T21, R10, T01):
191
+ """Find two matrices when slide is added to top of slab.
192
+
193
+ Compute the resulting 'R20' and 'T02' matrices for a glass slide
194
+ on top of an inhomogeneous layer characterized by 'R12', 'R21', 'T12',
195
+ 'T21' using:
196
+
197
+ .. math:: T_{02}=T_{12} (E-R_{10}R_{12})^{-1} T_{01}
198
+
199
+ .. math:: R_{20}=T_{12} (E-R_{10}R_{12})^{-1} R_{10} T_{21} + R_{21}
200
+
201
+ Args:
202
+ sample: Sample object
203
+ R12: reflection matrix for light moving downwards 1->2
204
+ R21: reflection matrix for light moving upwards 2->1
205
+ T12: transmission matrix for light moving downwards 1->2
206
+ T21: transmission matrix for light moving upwards 2->1
207
+ R10: reflection array for light moving upwards 0->1
208
+ T01: transmission array for light moving downwards 1->2
209
+
210
+ Returns:
211
+ R20, T02: resulting matrices for combined layers
212
+ """
213
+ n = sample.quad_pts
214
+ X = (np.identity(n) - R10 * R12.T).T
215
+ temp = np.linalg.solve(X.T, T12.T).T
216
+ T02 = temp * T01
217
+ R20 = (temp * R10) @ T21 + R21
218
+
219
+ return R20, T02
220
+
221
+
222
+ def _add_boundary_config_b(sample, R12, T21, R01, R10, T01, T10):
223
+ """Find two other matrices when slide is added to top of slab.
224
+
225
+ Compute the resulting 'R02' and 'T20' matrices for a glass slide
226
+ on top of an inhomogeneous layer characterized by 'R12', 'R21', 'T12',
227
+ 'T21' using:
228
+
229
+ .. math:: T_{20}=T_{10} (E-R_{12}R_{10})^{-1} T_{21}
230
+
231
+ .. math:: R_{02}=T_{10} (E-R_{12}R_{10})^{-1} R_{12} T_{01} + R_{01}
232
+
233
+ Args:
234
+ sample: Sample object
235
+ R12: reflection matrix for light moving downwards 1->2
236
+ T21: transmission matrix for light moving upwards 2->1
237
+ R01: reflection matrix for light moving downwards 0->1
238
+ R10: reflection matrix for light moving upwards 1->0
239
+ T01: transmission array for light moving downwards 1->2
240
+ T10: transmission array for light moving upwards 0->1
241
+
242
+ Returns:
243
+ R02, T20
244
+ """
245
+ n = sample.quad_pts
246
+ X = np.identity(n) - R12 * R10
247
+ temp = np.linalg.solve(X.T, np.diagflat(T10)).T
248
+ T20 = temp @ T21
249
+ R02 = (temp @ R12) * T01
250
+ R02 += np.diagflat(R01 / sample.twonuw**2)
251
+
252
+ return R02, T20
253
+
254
+
255
+ def add_slide_above(sample, R01, R10, T01, T10, R12, R21, T12, T21):
256
+ """Calculate matrices for a slab with a boundary placed above.
257
+
258
+ This routine should be used before the slide is been added below!
259
+
260
+ Here 0 is the air/top-of-slide, 1 is the bottom-of-slide/top-of-slab boundary,
261
+ and 2 is the is the bottom-of-slab boundary.
262
+
263
+ Args:
264
+ sample: Sample object
265
+ R01: reflection arrays for slide 0->1
266
+ R10: reflection arrays for slide 1->0
267
+ T01: transmission arrays for slide 0->1
268
+ T10: transmission arrays for slide 1->0
269
+ R12: reflection matrices for slab 1->2
270
+ R21: reflection matrices for slab 2->1
271
+ T12: transmission matrices for slab 1->2
272
+ T21: transmission matrices for slab 2->1
273
+
274
+ Returns:
275
+ R02, R20, T02, T20: matrices for slide+slab combination
276
+ """
277
+ R20, T02 = _add_boundary_config_a(sample, R12, R21, T12, T21, R10, T01)
278
+ R02, T20 = _add_boundary_config_b(sample, R12, T21, R01, R10, T01, T10)
279
+ return R02, R20, T02, T20
280
+
281
+
282
+ def add_slide_below(sample, R01, R10, T01, T10, R12, R21, T12, T21):
283
+ """Calculate matrices for a slab with a boundary placed below.
284
+
285
+ This routine should be used after the slide has been added to the top.
286
+
287
+ Here 0 is the top of slab, 1 is the bottom-of-slab/top-of-slide boundary,
288
+ and 2 is the is the bottom-of-slide/air boundary.
289
+
290
+ Args:
291
+ sample: Sample object
292
+ R01: reflection arrays for slide 0->1
293
+ R10: reflection arrays for slide 1->0
294
+ T01: transmission arrays for slide 0->1
295
+ T10: transmission arrays for slide 1->0
296
+ R12: reflection matrices for slab 1->2
297
+ R21: reflection matrices for slab 2->1
298
+ T12: transmission matrices for slab 1->2
299
+ T21: transmission matrices for slab 2->1
300
+
301
+ Returns:
302
+ R02, R20, T02, T20: matrices for slab+slide combination
303
+ """
304
+ R02, T20 = _add_boundary_config_a(sample, R10, R01, T10, T01, R12, T21)
305
+ R20, T02 = _add_boundary_config_b(sample, R10, T01, R21, R12, T21, T12)
306
+ return R02, R20, T02, T20
307
+
308
+
309
+ def add_same_slides(sample, R01, R10, T01, T10, R, T):
310
+ """Find matrix when slab is sandwiched between identical slides.
311
+
312
+ This routine is optimized for a slab with equal boundaries on each side.
313
+ It is assumed that the slab is homogeneous and therefore the 'R' and 'T'
314
+ matrices are identical for upward or downward light directions.
315
+
316
+ If equal boundary conditions exist on both sides of the slab then, by
317
+ symmetry, the transmission and reflection operator for light travelling
318
+ from the top to the bottom are equal to those for light propagating from
319
+ the bottom to the top. Consequently only one set need be calculated.
320
+ This leads to a faster method for calculating the reflection and
321
+ transmission for a slab with equal boundary conditions on each side.
322
+ Let the top boundary be layer 01, the medium layer 12, and the bottom
323
+ layer 23. The boundary conditions on each side are equal: R_{01}=R_{32},
324
+ R_{10}=R_{23}, T_{01}=T_{32}, and T_{10}=T_{23}.
325
+
326
+ For example the light reflected from layer 01 (travelling from boundary
327
+ 0 to boundary 1) will equal the amount of light reflected from layer 32,
328
+ since there is no physical difference between the two cases. The switch
329
+ in the numbering arises from the fact that light passes from the medium
330
+ to the outside at the top surface by going from 1 to 0, and from 2 to 3
331
+ on the bottom surface. The reflection and transmission for the slab
332
+ with boundary conditions are R_{30} and T_{03} respectively. These are
333
+ given by
334
+
335
+ .. math:: A_{XX} = T_{12}(E-R_{10}R_{12})^{-1}
336
+
337
+ .. math:: R_{20} = A_{XX} R_{10}T_{21} + R_{21}
338
+
339
+ .. math:: B_{XX} = T_{10}(E-R_{20}R_{10})^{-1}
340
+
341
+ .. math:: T_{03} = B_{XX} A_{XX} T_{01}
342
+
343
+ .. math:: R_{30} = B_{XX} R_{20} T_{01} + R_{01}/(2 𝜈 w)^2
344
+
345
+ Args:
346
+ sample: Sample object
347
+ R01: R for slide assuming 0=air and 1=slab
348
+ R10: R for slide assuming 0=air and 1=slab
349
+ T10: T for slide assuming 0=air and 1=slab
350
+ T01: T for slide assuming 0=air and 1=slab
351
+ R: R12=R21 for homogeneous slab
352
+ T: T12=T21 for homogeneous slab
353
+
354
+ Returns:
355
+ T30, T03: R, T for all 3 with top = bottom boundary
356
+ """
357
+ n = sample.quad_pts
358
+ X = np.identity(n) - R10 * R
359
+ AXX = np.linalg.solve(X, T.T).T
360
+ R20 = (AXX * R10) @ T + R
361
+
362
+ X = np.identity(n) - R20 * R10
363
+ BXX = scipy.linalg.solve(X.T, np.diagflat(T10)).T
364
+ T03 = BXX @ AXX * T01
365
+ R30 = BXX @ R20 * T01
366
+ R30 += np.diagflat(R01 / sample.twonuw**2)
367
+
368
+ return R30, T03
369
+
370
+
371
+ # ======================================================================
372
+ # Refractive-index-mismatched multilayer stack
373
+ # ======================================================================
374
+ #
375
+ # When adjacent layers have different refractive indices there is a real
376
+ # Fresnel interface between them. The strategy (see the module design notes)
377
+ # is to place *every* layer on one Snell-invariant master grid built by
378
+ # ``Sample._build_master_grid``. On that grid a physical channel ``j`` keeps
379
+ # a fixed invariant ``eta_j`` across all interfaces, so an interface is simply
380
+ # a *diagonal* operator in ``j`` — the same structure the air/slide boundaries
381
+ # already use. Each homogeneous layer is doubled on the sub-grid of the
382
+ # channels that actually propagate in it (the rest are totally internally
383
+ # reflected at its boundaries) and then embedded, with zeros, into the full
384
+ # master dimension.
385
+ #
386
+ # Because the stack is no longer up/down symmetric, all four matrices
387
+ # ``(R_down, R_up, T_down, T_up)`` are carried through the accumulation. Each
388
+ # interface and each layer is appended with the existing, tested ``add_layers``
389
+ # routine, driven by the twonuw of whichever grid the two blocks actually share
390
+ # (the interface's transmission carries the n^2-law factor between grids). The
391
+ # outer air/slide boundaries are added afterwards by ``Sample.rt_matrices``
392
+ # using the ordinary ``add_slide_above`` / ``add_slide_below`` on the first and
393
+ # last layer grids.
394
+
395
+
396
+ def _grid_sample(sample, n_k, grid, n_full):
397
+ """Clone ``sample`` configured to a single layer's master-grid slice.
398
+
399
+ The returned object carries the layer's scalar index and the full-length
400
+ (master-dimension) ``nu``/``twonuw`` so that it can drive the existing
401
+ boundary/adding routines, which key off ``sample.twonuw`` and
402
+ ``sample.quad_pts``.
403
+
404
+ Args:
405
+ sample: the multilayer sample being solved.
406
+ n_k: scalar refractive index of this layer/medium.
407
+ grid: ``(nu, twonuw, active)`` from ``Sample.layer_grid``.
408
+ n_full: number of master channels (``quad_pts``).
409
+ """
410
+ nu_k, twonuw_k, _ = grid
411
+ s = copy.deepcopy(sample)
412
+ nk = float(np.real(n_k))
413
+ s._n = nk
414
+ s.n_sample_boundary = nk
415
+ s._quad_pts = n_full
416
+ s.nu = nu_k
417
+ s.twonuw = twonuw_k
418
+ s.hp = None
419
+ s.hm = None
420
+ s.b_thinnest = None
421
+ s._layer_pf = None
422
+ return s
423
+
424
+
425
+ def _single_layer_embedded(sample, n_k, a_k, b_k, g_k, grid, n_full, pf=None):
426
+ """Doubled R/T of one homogeneous layer, embedded in the master dimension.
427
+
428
+ The layer is doubled only on the channels that propagate in it (``active``
429
+ mask), then scattered back into a full ``n_full`` x ``n_full`` matrix with
430
+ zeros on the inactive rows/columns. Inactive channels receive no flux
431
+ (the bounding interfaces transmit nothing into them), so leaving them zero
432
+ is exact.
433
+
434
+ Args:
435
+ pf: optional ``(pf_type, one_column_pf_data_or_None)`` tuple giving
436
+ this layer its own phase function; when omitted the layer scatters
437
+ Henyey-Greenstein with anisotropy ``g_k``.
438
+
439
+ Returns:
440
+ ``(R, T)`` full master-dimension matrices (symmetric for a single
441
+ homogeneous layer, so up == down).
442
+ """
443
+ import iadpython.ad as _ad
444
+
445
+ nu_k, twonuw_k, active = grid
446
+ idx = np.where(active)[0]
447
+
448
+ s = copy.deepcopy(sample)
449
+ s._n = float(np.real(n_k))
450
+ s.n_sample_boundary = float(np.real(n_k))
451
+ s.a = float(a_k)
452
+ s.b = float(b_k)
453
+ s._g = _ad.sanitize_anisotropy(float(g_k))
454
+ s._quad_pts = len(idx)
455
+ s.nu = nu_k[idx]
456
+ s.twonuw = twonuw_k[idx]
457
+ if pf is not None:
458
+ s.pf_type, s.pf_data = pf
459
+ s.hp = None
460
+ s.hm = None
461
+ s.b_thinnest = None
462
+ s._layer_pf = None
463
+
464
+ r_sub, t_sub = simple_single_layer_matrices(s)
465
+
466
+ R = np.zeros((n_full, n_full))
467
+ T = np.zeros((n_full, n_full))
468
+ R[np.ix_(idx, idx)] = r_sub
469
+ T[np.ix_(idx, idx)] = t_sub
470
+ return R, T
471
+
472
+
473
+ def _internal_interface_matrices(n_upper, grid_upper, n_lower, grid_lower, cache=None):
474
+ r"""Full diagonal R/T operators for the interface between two media.
475
+
476
+ The interface is a zero-thickness layer combined with the general
477
+ ``add_layers``. Written in the adding-doubling "layer" convention (where a
478
+ transparent layer is ``diag(1/twonuw)``), the diagonal operators are
479
+
480
+ * reflection: ``R_power / twonuw`` on the *incidence* grid,
481
+ * transmission: ``T_power / twonuw`` on the *incidence* grid.
482
+
483
+ Dividing the transmission by the incidence-side ``twonuw`` — rather than the
484
+ exit side — is exactly what encodes the :math:`n^2`-law of radiance across
485
+ the index step; it was verified against energy conservation. Total internal
486
+ reflection is automatic because ``interface_rt`` returns ``(R, T) = (1, 0)``
487
+ for any channel beyond the critical angle.
488
+
489
+ Args:
490
+ n_upper, n_lower: refractive indices of the two media.
491
+ grid_upper, grid_lower: ``(nu, twonuw, active)`` tuples for each side.
492
+
493
+ Returns:
494
+ ``(R12, R21, T12, T21)`` full diagonal matrices, where ``1`` labels the
495
+ upper medium and ``2`` the lower medium (``12`` = downward).
496
+ """
497
+ nu_u, tw_u, active_u = grid_upper
498
+ nu_l, tw_l, active_l = grid_lower
499
+
500
+ # Within one stack build, an index pair may recur (e.g. A|B|A|B ...). The
501
+ # per-build ``cache`` dict avoids recomputing those interfaces. It is not
502
+ # shared across builds, so it cannot go stale when the master grid changes.
503
+ key = (float(np.real(n_upper)), float(np.real(n_lower)))
504
+ if cache is not None and key in cache:
505
+ return cache[key]
506
+
507
+ r_ab, t_ab = iadpython.fresnel.interface_rt(n_upper, nu_u, n_lower)
508
+ r_ba, t_ba = iadpython.fresnel.interface_rt(n_lower, nu_l, n_upper)
509
+
510
+ # A channel that does not propagate in a medium must stay fully decoupled
511
+ # there, otherwise its placeholder weight injects spurious flux into the
512
+ # star products and the recursion diverges. Reflection is kept on the
513
+ # medium it returns to (this is the physical total-internal-reflection of
514
+ # trapped light); transmission survives only where *both* sides propagate.
515
+ both = active_u & active_l
516
+ r_ab = np.where(active_u, np.asarray(r_ab, dtype=float), 0.0)
517
+ r_ba = np.where(active_l, np.asarray(r_ba, dtype=float), 0.0)
518
+ t_ab = np.where(both, np.asarray(t_ab, dtype=float), 0.0)
519
+ t_ba = np.where(both, np.asarray(t_ba, dtype=float), 0.0)
520
+
521
+ R12 = np.diagflat(r_ab / tw_u)
522
+ R21 = np.diagflat(r_ba / tw_l)
523
+ T12 = np.diagflat(t_ab / tw_u)
524
+ T21 = np.diagflat(t_ba / tw_l)
525
+
526
+ result = (R12, R21, T12, T21)
527
+ if cache is not None:
528
+ cache[key] = result
529
+ return result
530
+
531
+
532
+ def _broadcast_layer_params(sample, n_layers):
533
+ """Return (a, b, g) each as length-``n_layers`` arrays."""
534
+ a = np.atleast_1d(sample.a)
535
+ b = np.atleast_1d(sample.b)
536
+ g = np.atleast_1d(sample.g)
537
+
538
+ def fit(x, name):
539
+ if x.size == 1:
540
+ return np.full(n_layers, x.flat[0])
541
+ if x.size != n_layers:
542
+ raise RuntimeError(
543
+ "mismatched_layer_matrices: %s has %d entries but n has %d layers"
544
+ % (name, x.size, n_layers)
545
+ )
546
+ return x
547
+
548
+ return fit(a, "a"), fit(b, "b"), fit(g, "g")
549
+
550
+
551
+ def mismatched_layer_matrices(sample):
552
+ """R/T matrices for a stack whose layers have different refractive indices.
553
+
554
+ Every layer is expressed on the shared Snell-invariant master grid; the
555
+ Fresnel interface between neighbouring layers is inserted as a diagonal
556
+ layer. Handles arbitrarily large index steps (via per-channel total
557
+ internal reflection) and reduces exactly to the single-index doubling when
558
+ all indices agree.
559
+
560
+ Args:
561
+ sample: a :class:`~iadpython.ad.Sample` with an array-valued ``n``.
562
+
563
+ Returns:
564
+ ``(R_down, R_up, T_down, T_up)`` full master-dimension matrices for the
565
+ bare stack (no external air/slide boundaries — those are added by
566
+ ``Sample.rt_matrices``). The top face is the first layer's medium and
567
+ the bottom face is the last layer's medium.
568
+ """
569
+ layer_pf = getattr(sample, "_layer_pf", None)
570
+ if layer_pf is None and str(getattr(sample, "pf_type", "HG")).upper() != "HG":
571
+ raise NotImplementedError(
572
+ "mismatched-index multilayer needs per-layer phase functions; "
573
+ "build the stack with iadpython.Sample(layers=[iadpython.Layer("
574
+ "pf_type='TABULATED', ...), ...]) or pf_type='MOMENTS'"
575
+ )
576
+
577
+ sample._build_master_grid()
578
+ n_full = sample._master_dim
579
+ ns = np.atleast_1d(sample.n)
580
+ n_layers = ns.size
581
+ a, b, g = _broadcast_layer_params(sample, n_layers)
582
+
583
+ def pf_of(k):
584
+ """Per-layer phase-function spec, or None for plain HG."""
585
+ if layer_pf is None:
586
+ return None
587
+ return layer_pf[k]
588
+
589
+ grids = [sample.layer_grid(ns[k]) for k in range(n_layers)]
590
+ interface_cache = {}
591
+
592
+ # First layer seeds the running stack (a homogeneous layer is symmetric).
593
+ R0, T0 = _single_layer_embedded(
594
+ sample, ns[0], a[0], b[0], g[0], grids[0], n_full, pf=pf_of(0)
595
+ )
596
+ r_down, r_up, t_down, t_up = R0, R0, T0, T0
597
+
598
+ for k in range(1, n_layers):
599
+ # (1) Add the Fresnel interface between layer k-1 and layer k below the
600
+ # running stack. The two blocks share the (k-1) grid, so the star
601
+ # product uses its twonuw. Afterwards the stack's bottom face lives
602
+ # on the k grid.
603
+ R12, R21, T12, T21 = _internal_interface_matrices(
604
+ ns[k - 1], grids[k - 1], ns[k], grids[k], cache=interface_cache
605
+ )
606
+ s_upper = _grid_sample(sample, ns[k - 1], grids[k - 1], n_full)
607
+ r_down, r_up, t_down, t_up = add_layers(
608
+ s_upper, r_down, r_up, t_down, t_up, R12, R21, T12, T21
609
+ )
610
+
611
+ # (2) Add layer k below the stack. They share the k grid.
612
+ Lk_R, Lk_T = _single_layer_embedded(
613
+ sample, ns[k], a[k], b[k], g[k], grids[k], n_full, pf=pf_of(k)
614
+ )
615
+ s_lower = _grid_sample(sample, ns[k], grids[k], n_full)
616
+ r_down, r_up, t_down, t_up = add_layers(
617
+ s_lower, r_down, r_up, t_down, t_up, Lk_R, Lk_R, Lk_T, Lk_T
618
+ )
619
+
620
+ return r_down, r_up, t_down, t_up
iadpython/constants.py ADDED
@@ -0,0 +1,3 @@
1
+ """List of constants used in inverse adding-doubling."""
2
+
3
+ AD_MAX_THICKNESS = 1e6