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/__init__.py +59 -0
- iadpython/ad.py +1377 -0
- iadpython/agrid.py +306 -0
- iadpython/cache.py +133 -0
- iadpython/combine.py +620 -0
- iadpython/constants.py +3 -0
- iadpython/data/M38597.csv +759 -0
- iadpython/double.py +376 -0
- iadpython/fresnel.py +642 -0
- iadpython/grid.py +125 -0
- iadpython/iad.py +615 -0
- iadpython/iadc.py +1003 -0
- iadpython/iadcommand.py +712 -0
- iadpython/layer.py +241 -0
- iadpython/nist.py +114 -0
- iadpython/port.py +242 -0
- iadpython/quadrature.py +172 -0
- iadpython/redistribution.py +318 -0
- iadpython/rxt.py +201 -0
- iadpython/sphere.py +582 -0
- iadpython/start.py +377 -0
- iadpython/txt.py +147 -0
- radphen_iadpython-0.7.0.dist-info/METADATA +264 -0
- radphen_iadpython-0.7.0.dist-info/RECORD +28 -0
- radphen_iadpython-0.7.0.dist-info/WHEEL +5 -0
- radphen_iadpython-0.7.0.dist-info/entry_points.txt +2 -0
- radphen_iadpython-0.7.0.dist-info/licenses/LICENSE.txt +18 -0
- radphen_iadpython-0.7.0.dist-info/top_level.txt +1 -0
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