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/fresnel.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# pylint: disable=consider-using-in
|
|
2
|
+
# pylint: disable=arguments-out-of-order
|
|
3
|
+
|
|
4
|
+
"""Module for generating boundary matrices.
|
|
5
|
+
|
|
6
|
+
Two types of starting methods are possible.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> import iadpython as iad
|
|
10
|
+
>>> n = 4
|
|
11
|
+
>>> slab = iad.Slab(a = 0.9, b = 10, g = 0.9, n = 1.5)
|
|
12
|
+
>>> method = iad.Method(slab)
|
|
13
|
+
>>> r, t = iad.init_layer(slab, method)
|
|
14
|
+
>>> print(r)
|
|
15
|
+
>>> print(t)
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import iadpython.constants
|
|
21
|
+
|
|
22
|
+
__all__ = (
|
|
23
|
+
"cos_critical",
|
|
24
|
+
"cos_snell",
|
|
25
|
+
"fresnel_reflection",
|
|
26
|
+
"absorbing_glass_RT",
|
|
27
|
+
"specular_rt",
|
|
28
|
+
"diffuse_glass_R",
|
|
29
|
+
"glass",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_complex_value(*values, tol=1e-12):
|
|
34
|
+
"""Return True when an input carries a meaningful imaginary part."""
|
|
35
|
+
for value in values:
|
|
36
|
+
if np.isscalar(value):
|
|
37
|
+
if abs(complex(value).imag) > tol:
|
|
38
|
+
return True
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
arr = np.asarray(value)
|
|
42
|
+
if not np.iscomplexobj(arr):
|
|
43
|
+
continue
|
|
44
|
+
if np.any(np.abs(arr.imag) > tol):
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _transport_index(value):
|
|
50
|
+
"""Use the real index for transport-angle geometry."""
|
|
51
|
+
return float(np.real(value))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sanitize_path_cos(nu):
|
|
55
|
+
"""Positive path-length cosine used for Beer-Lambert attenuation."""
|
|
56
|
+
nu_arr = np.asarray(np.real(nu), dtype=float)
|
|
57
|
+
out = np.maximum(nu_arr, 1e-12)
|
|
58
|
+
if np.isscalar(nu) or out.ndim == 0:
|
|
59
|
+
return float(out)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_near_zero(value, tol=1e-14):
|
|
64
|
+
"""Cheap absolute zero test for scalar or array values."""
|
|
65
|
+
return np.abs(np.asarray(value)) <= tol
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def interface_rt(n_i, nu_i, n_t):
|
|
69
|
+
"""Power reflectance/transmittance for one interface.
|
|
70
|
+
|
|
71
|
+
Supports both real and complex refractive indices. For complex media the
|
|
72
|
+
transmitted power is obtained from the real Poynting-flux ratio, consistent
|
|
73
|
+
with the wave-optics boundary formulas used elsewhere in the stack.
|
|
74
|
+
"""
|
|
75
|
+
# Identical media have no interface. Evaluate this analytic limit before
|
|
76
|
+
# Snell's law and the flux ratios lose precision or become 0/0 at grazing.
|
|
77
|
+
# Exact equality matters: even a small index contrast reflects at grazing.
|
|
78
|
+
if (
|
|
79
|
+
np.ndim(n_i) == 0
|
|
80
|
+
and np.ndim(n_t) == 0
|
|
81
|
+
and n_i == n_t
|
|
82
|
+
and np.isfinite(n_i)
|
|
83
|
+
and np.real(n_i) > 0
|
|
84
|
+
):
|
|
85
|
+
if np.ndim(nu_i) == 0:
|
|
86
|
+
return 0.0, 1.0
|
|
87
|
+
return np.zeros(np.shape(nu_i)), np.ones(np.shape(nu_i))
|
|
88
|
+
|
|
89
|
+
use_complex = _is_complex_value(n_i, n_t, nu_i)
|
|
90
|
+
if use_complex:
|
|
91
|
+
nu_i_arr = np.asarray(nu_i, dtype=complex)
|
|
92
|
+
else:
|
|
93
|
+
n_i = float(np.real(n_i))
|
|
94
|
+
n_t = float(np.real(n_t))
|
|
95
|
+
nu_i_arr = np.asarray(nu_i, dtype=float)
|
|
96
|
+
nu_t = cos_snell(n_i, nu_i_arr, n_t)
|
|
97
|
+
|
|
98
|
+
denom_tm = n_t * nu_i_arr + n_i * nu_t
|
|
99
|
+
denom_te = n_i * nu_i_arr + n_t * nu_t
|
|
100
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
101
|
+
r_tm = (n_t * nu_i_arr - n_i * nu_t) / denom_tm
|
|
102
|
+
r_te = (n_i * nu_i_arr - n_t * nu_t) / denom_te
|
|
103
|
+
t_tm = (2.0 * n_i * nu_i_arr) / denom_tm
|
|
104
|
+
t_te = (2.0 * n_i * nu_i_arr) / denom_te
|
|
105
|
+
|
|
106
|
+
denom_tm_zero = _is_near_zero(denom_tm)
|
|
107
|
+
denom_te_zero = _is_near_zero(denom_te)
|
|
108
|
+
r_tm = np.where(denom_tm_zero, 1.0 + 0.0j, r_tm)
|
|
109
|
+
r_te = np.where(denom_te_zero, 1.0 + 0.0j, r_te)
|
|
110
|
+
t_tm = np.where(denom_tm_zero, 0.0 + 0.0j, t_tm)
|
|
111
|
+
t_te = np.where(denom_te_zero, 0.0 + 0.0j, t_te)
|
|
112
|
+
|
|
113
|
+
R = 0.5 * (np.abs(r_tm) ** 2 + np.abs(r_te) ** 2)
|
|
114
|
+
|
|
115
|
+
flux_in_tm = np.real(np.conj(n_i) * nu_i_arr)
|
|
116
|
+
flux_out_tm = np.real(np.conj(n_t) * nu_t)
|
|
117
|
+
flux_in_te = np.real(n_i * nu_i_arr)
|
|
118
|
+
flux_out_te = np.real(n_t * nu_t)
|
|
119
|
+
|
|
120
|
+
flux_in_tm_zero = _is_near_zero(flux_in_tm)
|
|
121
|
+
flux_in_te_zero = _is_near_zero(flux_in_te)
|
|
122
|
+
|
|
123
|
+
T_tm = np.divide(
|
|
124
|
+
flux_out_tm * np.abs(t_tm) ** 2,
|
|
125
|
+
flux_in_tm,
|
|
126
|
+
out=np.zeros_like(R, dtype=float),
|
|
127
|
+
where=~flux_in_tm_zero,
|
|
128
|
+
)
|
|
129
|
+
T_te = np.divide(
|
|
130
|
+
flux_out_te * np.abs(t_te) ** 2,
|
|
131
|
+
flux_in_te,
|
|
132
|
+
out=np.zeros_like(R, dtype=float),
|
|
133
|
+
where=~flux_in_te_zero,
|
|
134
|
+
)
|
|
135
|
+
T = 0.5 * (T_tm + T_te)
|
|
136
|
+
T = np.where(T < 0.0, 0.0, T)
|
|
137
|
+
|
|
138
|
+
if np.isscalar(nu_i) or np.ndim(nu_i) == 0:
|
|
139
|
+
return float(np.asarray(R).reshape(-1)[0]), float(np.asarray(T).reshape(-1)[0])
|
|
140
|
+
return np.asarray(R, dtype=float), np.asarray(T, dtype=float)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cos_critical(n_i, n_t):
|
|
144
|
+
r"""Calculate the cosine of the critical angle.
|
|
145
|
+
|
|
146
|
+
This works for arrays too. If there is no critical angle then
|
|
147
|
+
cos(\pi/2)=0 is returned.
|
|
148
|
+
|
|
149
|
+
.. math:: \theta_c = \sin^{-1}(n_t / n_i)
|
|
150
|
+
|
|
151
|
+
The cosine of this angle is then
|
|
152
|
+
|
|
153
|
+
.. math:: \cos(\theta_c) = \cos(\sin^{-1}(n_t / n_i))
|
|
154
|
+
|
|
155
|
+
.. math:: \cos(\theta_c) = \sqrt{1-(n_t/n_i)^2}
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
n_i: index of refraction of incident medium
|
|
159
|
+
n_t: index of refraction of transmitted medium
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
cosine of the critical angle
|
|
163
|
+
"""
|
|
164
|
+
if _is_complex_value(n_i, n_t):
|
|
165
|
+
return 0.0
|
|
166
|
+
|
|
167
|
+
temp = 1.0 - (n_t / n_i) ** 2
|
|
168
|
+
|
|
169
|
+
if not np.isscalar(temp):
|
|
170
|
+
np.place(temp, temp < 0, 0)
|
|
171
|
+
elif temp < 0:
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
return np.sqrt(temp)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def cos_snell(n_i, nu_i, n_t):
|
|
178
|
+
r"""Return the cosine of the transmitted angle.
|
|
179
|
+
|
|
180
|
+
Snell's law states
|
|
181
|
+
|
|
182
|
+
.. math:: n_i\sin(\theta_i) = n_t \sin(\theta_t)
|
|
183
|
+
|
|
184
|
+
but if the angles are expressed as cosines,
|
|
185
|
+
:math:`\nu_i = \cos(\theta_i)` then
|
|
186
|
+
|
|
187
|
+
.. math:: n_i\sin(\cos^{-1}\nu_i) = n_t \sin(\cos^{-1}\nu_t)
|
|
188
|
+
|
|
189
|
+
Solving for :math:`\nu_t` yields
|
|
190
|
+
|
|
191
|
+
.. math:: \nu_t = \cos(\sin^{-1}[(n_i/n_t) \sin(\cos^{-1}\nu_i)])
|
|
192
|
+
|
|
193
|
+
which is pretty ugly. However, note that
|
|
194
|
+
|
|
195
|
+
.. math:: \sin(\cos^{-1}\nu) = \sqrt{1-\nu^2}
|
|
196
|
+
|
|
197
|
+
and the above becomes
|
|
198
|
+
|
|
199
|
+
.. math:: \nu_t = \sqrt{1-(n_i/n_t)^2 (1- \nu_i^2)}
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
n_i: index of refraction of incident medium
|
|
203
|
+
nu_i: cosine of angle of incidence
|
|
204
|
+
n_t: index of refraction of transmitted medium
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
cosine of transmitted angle
|
|
208
|
+
"""
|
|
209
|
+
if _is_complex_value(n_i, n_t, nu_i):
|
|
210
|
+
temp = 1.0 - (n_i / n_t) ** 2 * (1.0 - np.asarray(nu_i, dtype=complex) ** 2)
|
|
211
|
+
out = np.sqrt(temp + 0j)
|
|
212
|
+
if np.isscalar(nu_i) or np.ndim(nu_i) == 0:
|
|
213
|
+
return complex(np.asarray(out).reshape(-1)[0])
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
n_i = float(np.real(n_i))
|
|
217
|
+
n_t = float(np.real(n_t))
|
|
218
|
+
temp = 1.0 - (n_i / n_t) ** 2 * (1.0 - nu_i**2)
|
|
219
|
+
|
|
220
|
+
if not np.isscalar(temp):
|
|
221
|
+
np.place(temp, temp < 0, 0)
|
|
222
|
+
elif temp < 0:
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
return np.sqrt(temp)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def fresnel_reflection(n_i, nu_i, n_t):
|
|
229
|
+
r"""Fresnel Reflection.
|
|
230
|
+
|
|
231
|
+
Calculates the specular reflection for light incident at
|
|
232
|
+
an angle theta_i from the normal (having a cosine equal to nu_i)
|
|
233
|
+
in a medium with index of
|
|
234
|
+
refraction 'n_i' onto a medium with index of refraction 'n_t' .
|
|
235
|
+
|
|
236
|
+
The usual way to calculate the total reflection for unpolarized light is
|
|
237
|
+
to use the Fresnel formula
|
|
238
|
+
|
|
239
|
+
.. math::
|
|
240
|
+
|
|
241
|
+
R = \frac{1}{2} \left[\frac{\sin^2(\theta_i-\theta_t)}{\sin^2(\theta_i+\theta_t)} +
|
|
242
|
+
\frac{\tan^2(\theta_i-\theta_t)}{\tan^2(\theta_i+\theta_t)} \right]
|
|
243
|
+
|
|
244
|
+
where theta_i and theta_t represent the angle (from normal) that light is incident
|
|
245
|
+
and the angle at which light is transmitted.
|
|
246
|
+
There are several problems with calculating the reflection using this formula.
|
|
247
|
+
First, if the angle of incidence is zero, then the formula results in division by zero.
|
|
248
|
+
Furthermore, if the angle of incidence is near zero, then the formula is the ratio
|
|
249
|
+
of two small numbers and the results can be inaccurate.
|
|
250
|
+
Second, if the angle of incidence exceeds the critical angle, then the calculation of
|
|
251
|
+
theta_t results in an attempt to find the arcsine of a quantity greater than
|
|
252
|
+
one. Third, all calculations in this program are based on the cosine of the angle.
|
|
253
|
+
This routine forces the calling routine to find theta_i = cos**-1 nu.
|
|
254
|
+
Fourth, the routine also gives problems when the critical angle is exceeded.
|
|
255
|
+
|
|
256
|
+
Closer inspection reveals that this is the wrong formulation to use. The formulas that
|
|
257
|
+
should be used for parallel and perpendicular polarization are
|
|
258
|
+
|
|
259
|
+
.. math::
|
|
260
|
+
|
|
261
|
+
R_\parallel = \left[\frac{n_t\cos\theta_i-n_i\cos\theta_t}
|
|
262
|
+
{n_t\cos\theta_i+n_i\cos\theta_t}\right]^2,
|
|
263
|
+
|
|
264
|
+
.. math::
|
|
265
|
+
|
|
266
|
+
R_\perp = \left[\frac{n_i\cos\theta_i-n_t\cos\theta_t}
|
|
267
|
+
{n_i\cos\theta_i+n_t\cos\theta_t}\right]^2.
|
|
268
|
+
|
|
269
|
+
The formula for unpolarized light, written in terms of
|
|
270
|
+
:math:`\nu_i = \cos \theta_i` and :math:`\nu_t = \cos \theta_t` is
|
|
271
|
+
|
|
272
|
+
.. math::
|
|
273
|
+
|
|
274
|
+
R = \frac{1}{2} \left[\frac{n_t\nu_i-n_i\nu_t}{n_t \nu_i+n_i \nu_t}\right]^2 +
|
|
275
|
+
\frac{1}{2} \left[\frac{n_i\nu_i-n_t\nu_t}{n_i \nu_i+n_t \nu_t}\right]^2
|
|
276
|
+
|
|
277
|
+
This formula has the advantage that no trig routines need to be called and that the
|
|
278
|
+
case of normal irradiance does not cause division by zero. Near normal incidence
|
|
279
|
+
remains numerically well-conditioned. In the routine below, I test for matched
|
|
280
|
+
boundaries and normal incidence to eliminate unnecessary calculations. I also
|
|
281
|
+
test for total internal reflection to avoid possible division by zero. I also
|
|
282
|
+
find the ratio of the indices of refraction to avoid an extra multiplication and
|
|
283
|
+
several intermediate variables.
|
|
284
|
+
|
|
285
|
+
The tricky things about this implementation are to get handle angles above
|
|
286
|
+
the critical angle properly and trying to keep everything working when
|
|
287
|
+
arrays are passed. If n_i==n_t then we want to return zero, otherwise
|
|
288
|
+
we want to return 1.
|
|
289
|
+
"""
|
|
290
|
+
return interface_rt(n_i, nu_i, n_t)[0]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def glass(n_i, n_g, n_t, nu_i):
|
|
294
|
+
r"""Reflection from a glass slide.
|
|
295
|
+
|
|
296
|
+
'glass' calculates the total specular reflection (i.e., including
|
|
297
|
+
multiple internal reflections) based on
|
|
298
|
+
the indices of refraction of the incident medium 'n_i', the glass 'n_g',
|
|
299
|
+
and medium into which the light is transmitted 'n_t' for light incident at
|
|
300
|
+
an angle from the normal having cosine 'nu_i'.
|
|
301
|
+
|
|
302
|
+
In many tissue optics problems, the sample is constrained by a piece of glass
|
|
303
|
+
creating an air-glass-tissue sequence.
|
|
304
|
+
The adding-doubling formalism can calculate the effect that the layer of glass will
|
|
305
|
+
have on the radiative transport properties by including a layer for the glass-tissue
|
|
306
|
+
interface and a layer for the air-glass interface. However, it is simpler to find net
|
|
307
|
+
effect of the glass slide and include only one layer for the glass boundary.
|
|
308
|
+
|
|
309
|
+
The first time I implemented this routine, I did not include multiple internal
|
|
310
|
+
reflections. After running test cases, it soon became apparent that the
|
|
311
|
+
percentage errors were way too
|
|
312
|
+
big for media with little absorption and scattering. It is not hard to find the
|
|
313
|
+
result for the reflection from a non-absorbing glass layer (equation A2.21
|
|
314
|
+
in my dissertation) in which multiple reflections are properly accounted for
|
|
315
|
+
|
|
316
|
+
.. math:: r_g = \frac{r_1 + r_2 - 2 r_1 r_2 }{ 1 - r_1 r_2}
|
|
317
|
+
|
|
318
|
+
Here :math:`r_1` is the reflection at the air-glass interface and :math:`r_2` is the
|
|
319
|
+
reflection at the glass-sample interface.
|
|
320
|
+
|
|
321
|
+
There is one pitfall in calculating r_g. When the angle
|
|
322
|
+
of incidence exceeds the critical angle then the formula above causes
|
|
323
|
+
division by zero. If this is the case then r_1 = 1 and can easily
|
|
324
|
+
be tested for.
|
|
325
|
+
|
|
326
|
+
To eliminate unnecessary computation, I check to make sure that
|
|
327
|
+
it really is necessary to call the 'Fresnel' routine twice.
|
|
328
|
+
It is noteworthy that the formula for r_g works correctly if the
|
|
329
|
+
the first boundary is not totally reflecting but the second one is.
|
|
330
|
+
Note that nu_g gets calculated twice
|
|
331
|
+
(once in the first call to 'Fresnel' and once directly).
|
|
332
|
+
"""
|
|
333
|
+
if n_i == n_g or n_g == n_t:
|
|
334
|
+
return fresnel_reflection(n_i, nu_i, n_t)
|
|
335
|
+
|
|
336
|
+
r1 = fresnel_reflection(n_i, nu_i, n_g)
|
|
337
|
+
nu_g = cos_snell(n_i, nu_i, n_g)
|
|
338
|
+
r2 = fresnel_reflection(n_g, nu_g, n_t)
|
|
339
|
+
denom = 1 - r1 * r2
|
|
340
|
+
numer = r1 + r2 - 2 * r1 * r2
|
|
341
|
+
|
|
342
|
+
if np.isscalar(denom):
|
|
343
|
+
if numer == denom:
|
|
344
|
+
return 1
|
|
345
|
+
return numer / denom
|
|
346
|
+
|
|
347
|
+
return np.divide(numer, denom, out=np.ones_like(numer), where=numer != denom)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def absorbing_glass_RT(n_i, n_g, n_t, nu_i, b):
|
|
351
|
+
r"""Reflection and transmission of an absorbing slide.
|
|
352
|
+
|
|
353
|
+
Calculates the total specular reflection and transmission
|
|
354
|
+
(i.e., including multiple internal reflections) based on
|
|
355
|
+
the indices of refraction of the incident medium 'n_i', the glass 'n_g',
|
|
356
|
+
and medium into which the light is transmitted 'n_t' for light incident at
|
|
357
|
+
an angle from the normal having cosine 'nu_i'. The optical thickness of
|
|
358
|
+
the glass b = nu_a d is measured normal to the glass.
|
|
359
|
+
|
|
360
|
+
This routine was generated to help solve a problem with the inverse adding-doubling
|
|
361
|
+
program associated with samples with low absorbances. A particular situation
|
|
362
|
+
(in the IR) arises when the slides have significant absorption relative to the sample
|
|
363
|
+
absorption. Anyway, it is not hard to extend the result for non-absorbing slides
|
|
364
|
+
to the absorbing case
|
|
365
|
+
|
|
366
|
+
.. math:: r = r_1 + \frac{(1-r_1)^2 r_2 e^{-2b/\nu_g}}{1 - r_1 r_2e^{-2b/\nu_g}}
|
|
367
|
+
|
|
368
|
+
Here r_1 is the reflection at the sample-glass interface and r_2 is the
|
|
369
|
+
reflection at the glass-air interface and nu_g is the cosine of the
|
|
370
|
+
angle inside the glass. Note that if b≠0 then the reflection depends
|
|
371
|
+
on the order of the indices of refraction, otherwise 'n_i' and 'n_t'
|
|
372
|
+
can be switched and the result should be the same.
|
|
373
|
+
|
|
374
|
+
The corresponding result for transmission is
|
|
375
|
+
|
|
376
|
+
.. math:: t = \frac{(1-r_1)(1-r_2)e^{-b/\nu_g}} {1 - r_1 r_2e^{-2b/\nu_g}}
|
|
377
|
+
|
|
378
|
+
There are two potential pitfalls in the calculation. The first is
|
|
379
|
+
when the angle of incidence exceeds the critical angle then the formula causes
|
|
380
|
+
division by zero. If this is the case, 'Fresnel' will return r_1 = 1 and
|
|
381
|
+
this routine responds appropriately. The second case is when the optical
|
|
382
|
+
thickness of the slide is too large.
|
|
383
|
+
|
|
384
|
+
I don't worry too much about optimal coding, because this routine does
|
|
385
|
+
not get called all that often and also because 'Fresnel' is pretty good
|
|
386
|
+
at avoiding unnecessary computations. At worst this routine just has
|
|
387
|
+
a couple of extra function calls and a few extra multiplications.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
n_i: index of medium from which light is incident
|
|
391
|
+
n_g: index of glass
|
|
392
|
+
n_t: index of slab
|
|
393
|
+
nu_i: cosine of angle of incidence (in n_i)
|
|
394
|
+
b: optical thickness of glass
|
|
395
|
+
Returns
|
|
396
|
+
r, t: unscattered reflectance(s) and transmission(s)
|
|
397
|
+
"""
|
|
398
|
+
if abs(b) <= 1e-14 and (
|
|
399
|
+
np.isclose(np.real(n_g), 1.0) or n_i == n_g or n_g == n_t
|
|
400
|
+
):
|
|
401
|
+
return interface_rt(n_i, nu_i, n_t)
|
|
402
|
+
|
|
403
|
+
r12, t12 = interface_rt(n_i, nu_i, n_g)
|
|
404
|
+
nu_g = cos_snell(n_i, nu_i, n_g)
|
|
405
|
+
|
|
406
|
+
# too thick for any light to make it through the slide
|
|
407
|
+
if np.isscalar(b) and b > iadpython.AD_MAX_THICKNESS:
|
|
408
|
+
return r12, np.zeros_like(r12)
|
|
409
|
+
|
|
410
|
+
r21, t21 = interface_rt(n_g, nu_g, n_i)
|
|
411
|
+
r23, t23 = interface_rt(n_g, nu_g, n_t)
|
|
412
|
+
|
|
413
|
+
expo = np.exp(-b / _sanitize_path_cos(nu_g))
|
|
414
|
+
denom = 1.0 - r21 * r23 * expo**2
|
|
415
|
+
r = r12 + np.divide(
|
|
416
|
+
t12 * r23 * expo**2 * t21,
|
|
417
|
+
denom,
|
|
418
|
+
out=np.ones_like(np.asarray(r12, dtype=float)),
|
|
419
|
+
where=~_is_near_zero(denom),
|
|
420
|
+
)
|
|
421
|
+
t = np.divide(
|
|
422
|
+
t12 * expo * t23,
|
|
423
|
+
denom,
|
|
424
|
+
out=np.zeros_like(np.asarray(t12, dtype=float)),
|
|
425
|
+
where=~_is_near_zero(denom),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
if np.isscalar(nu_i) or np.ndim(nu_i) == 0:
|
|
429
|
+
return float(np.asarray(r).reshape(-1)[0]), float(np.asarray(t).reshape(-1)[0])
|
|
430
|
+
return np.asarray(r, dtype=float), np.asarray(t, dtype=float)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _specular_rt(
|
|
434
|
+
n_top,
|
|
435
|
+
n_slab,
|
|
436
|
+
n_bot,
|
|
437
|
+
b_slab,
|
|
438
|
+
nu,
|
|
439
|
+
b_top=0,
|
|
440
|
+
b_bot=0,
|
|
441
|
+
*,
|
|
442
|
+
n_outer_top=1.0,
|
|
443
|
+
n_outer_bot=1.0,
|
|
444
|
+
n_slab_transport=None,
|
|
445
|
+
):
|
|
446
|
+
"""Unscattered reflection and transmission through a glass-slab-glass sandwich.
|
|
447
|
+
|
|
448
|
+
Light is incident at nu=cos(theta) in the external medium n_outer_top
|
|
449
|
+
onto an absorbing glass plate, then a slab and another glass plate,
|
|
450
|
+
before exiting into n_outer_bot. The external media default to air.
|
|
451
|
+
|
|
452
|
+
If r_top and r_bottom are the reflectances for the top and bottom then
|
|
453
|
+
|
|
454
|
+
r = r_top + r_bottom*t_top**2*expo**2 / [1-r_top*r_bottom*expo**2]
|
|
455
|
+
|
|
456
|
+
and the transmission is
|
|
457
|
+
|
|
458
|
+
t = t_top*t_bottom*expo / [1-r_top*r_bottom*expo**2]
|
|
459
|
+
|
|
460
|
+
where expo = exp(-b_slab/nu_slab), using the cosine inside the slab.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
n_top: index of glass slide on top
|
|
464
|
+
n_slab: index of the slab
|
|
465
|
+
n_bot: index of glass on bottom
|
|
466
|
+
b_slab: optical thickness of the slab
|
|
467
|
+
nu: cosine of incident angle(s) in the external medium n_outer_top
|
|
468
|
+
b_top: optical thickness of top slide
|
|
469
|
+
b_bot: optical thickness of the bottom slide
|
|
470
|
+
Returns
|
|
471
|
+
r, t: unscattered reflectance(s) and transmission(s)
|
|
472
|
+
"""
|
|
473
|
+
if n_slab_transport is None:
|
|
474
|
+
n_slab_transport = _transport_index(n_slab)
|
|
475
|
+
|
|
476
|
+
nu_slab = cos_snell(_transport_index(n_outer_top), nu, n_slab_transport)
|
|
477
|
+
|
|
478
|
+
r_top, t_top = absorbing_glass_RT(n_outer_top, n_top, n_slab, nu, b_top)
|
|
479
|
+
r_top_rev, t_top_rev = absorbing_glass_RT(n_slab, n_top, n_outer_top, nu_slab, b_top)
|
|
480
|
+
|
|
481
|
+
if np.isscalar(b_slab) and b_slab > iadpython.AD_MAX_THICKNESS:
|
|
482
|
+
return r_top, 0.0
|
|
483
|
+
|
|
484
|
+
r_bottom, t_bottom = absorbing_glass_RT(n_slab, n_bot, n_outer_bot, nu_slab, b_bot)
|
|
485
|
+
|
|
486
|
+
expo = np.exp(-b_slab / _sanitize_path_cos(nu_slab))
|
|
487
|
+
denom = 1 - r_top_rev * r_bottom * expo**2
|
|
488
|
+
r = r_top + np.divide(
|
|
489
|
+
t_top * r_bottom * expo**2 * t_top_rev,
|
|
490
|
+
denom,
|
|
491
|
+
out=np.ones_like(np.asarray(r_top, dtype=float)),
|
|
492
|
+
where=~_is_near_zero(denom),
|
|
493
|
+
)
|
|
494
|
+
t = np.divide(
|
|
495
|
+
t_top * expo * t_bottom,
|
|
496
|
+
denom,
|
|
497
|
+
out=np.zeros_like(np.asarray(t_top, dtype=float)),
|
|
498
|
+
where=~_is_near_zero(denom),
|
|
499
|
+
)
|
|
500
|
+
return r, t
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def specular_rt(
|
|
504
|
+
n_top,
|
|
505
|
+
n_slab,
|
|
506
|
+
n_bot,
|
|
507
|
+
b_slab,
|
|
508
|
+
nu,
|
|
509
|
+
b_top=0,
|
|
510
|
+
b_bot=0,
|
|
511
|
+
flip=False,
|
|
512
|
+
*,
|
|
513
|
+
n_outer_top=1.0,
|
|
514
|
+
n_outer_bot=1.0,
|
|
515
|
+
n_slab_transport=None,
|
|
516
|
+
):
|
|
517
|
+
"""Unscattered refl and trans for a sample.
|
|
518
|
+
|
|
519
|
+
Find the reflectance to incorporate flipping of the sample. This
|
|
520
|
+
is needed when the sample is flipped between measurements.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
n_top: index of glass slide on top
|
|
524
|
+
n_slab: index of the slab
|
|
525
|
+
n_bot: index of glass on bottom
|
|
526
|
+
b_slab: optical thickness of the slab
|
|
527
|
+
nu: cosine of incident angle(s) in the external medium on the
|
|
528
|
+
illuminated side: n_outer_top, or n_outer_bot when flip=True
|
|
529
|
+
b_top: optical thickness of top slide
|
|
530
|
+
b_bot: optical thickness of the bottom slide
|
|
531
|
+
flip: True if light hits bottom first
|
|
532
|
+
Returns
|
|
533
|
+
r, t: unscattered reflectance(s) and transmission(s)
|
|
534
|
+
"""
|
|
535
|
+
if flip:
|
|
536
|
+
return _specular_rt(
|
|
537
|
+
n_bot,
|
|
538
|
+
n_slab,
|
|
539
|
+
n_top,
|
|
540
|
+
b_slab,
|
|
541
|
+
nu,
|
|
542
|
+
b_bot,
|
|
543
|
+
b_top,
|
|
544
|
+
n_outer_top=n_outer_bot,
|
|
545
|
+
n_outer_bot=n_outer_top,
|
|
546
|
+
n_slab_transport=n_slab_transport,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
return _specular_rt(
|
|
550
|
+
n_top,
|
|
551
|
+
n_slab,
|
|
552
|
+
n_bot,
|
|
553
|
+
b_slab,
|
|
554
|
+
nu,
|
|
555
|
+
b_top,
|
|
556
|
+
b_bot,
|
|
557
|
+
n_outer_top=n_outer_top,
|
|
558
|
+
n_outer_bot=n_outer_bot,
|
|
559
|
+
n_slab_transport=n_slab_transport,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def R1(n_i, n_t):
|
|
564
|
+
r"""Calculate the total diffuse reflection using the formula by Walsh.
|
|
565
|
+
|
|
566
|
+
This function computes the first moment of the Fresnel reflectance (R₁) based on
|
|
567
|
+
the analytical solution developed by Walsh. The formula used for R₁ is:
|
|
568
|
+
|
|
569
|
+
.. math::
|
|
570
|
+
|
|
571
|
+
R₁ = 1/2 + \\frac{(m-1)(3m+1)}{6(m+1)²}
|
|
572
|
+
+ \\frac{m²(m²-1)²}{(m²+1)³} \\log\\left(\\frac{m-1}{m+1}\\right)
|
|
573
|
+
- \\frac{2m³(m²+2m-1)}{(m²+1)(m⁴-1)}
|
|
574
|
+
+ \\frac{8m⁴(m⁴+1)}{(m²+1)(m⁴-1)²} \\log(m)
|
|
575
|
+
|
|
576
|
+
where Walsh's parameter m = n_t / n_i. This equation is valid when n_i < n_t.
|
|
577
|
+
|
|
578
|
+
If n_i > n_t, you can use the following relationship (see Egan and Hilgeman 1973):
|
|
579
|
+
|
|
580
|
+
.. math::
|
|
581
|
+
|
|
582
|
+
R(1/m) = 1 - m²[1 - R(m)]
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
n_i: The refractive index of the incident medium.
|
|
586
|
+
n_t: The refractive index of the transmitting medium.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
float: The calculated total diffuse reflection (R₁).
|
|
590
|
+
|
|
591
|
+
References:
|
|
592
|
+
- Walsh's analytical solution: [see Ryde 1931]
|
|
593
|
+
- Relationship for n_i > n_t: [see Egan and Hilgeman 1973]
|
|
594
|
+
"""
|
|
595
|
+
if n_i == n_t:
|
|
596
|
+
return 0.0
|
|
597
|
+
|
|
598
|
+
if n_i < n_t:
|
|
599
|
+
m = n_t / n_i
|
|
600
|
+
else:
|
|
601
|
+
m = n_i / n_t
|
|
602
|
+
|
|
603
|
+
m2 = m * m
|
|
604
|
+
m4 = m2 * m2
|
|
605
|
+
mm1 = m - 1
|
|
606
|
+
mp1 = m + 1
|
|
607
|
+
temp = (m2 - 1) / (m2 + 1)
|
|
608
|
+
|
|
609
|
+
r = 0.5 + mm1 * (3 * m + 1) / 6 / mp1 / mp1
|
|
610
|
+
r += m2 * temp**2 / (m2 + 1) * np.log(mm1 / mp1)
|
|
611
|
+
r -= 2 * m * m2 * (m2 + 2 * m - 1) / (m2 + 1) / (m4 - 1)
|
|
612
|
+
r += 8 * m4 * (m4 + 1) / (m2 + 1) / (m4 - 1) / (m4 - 1) * np.log(m)
|
|
613
|
+
|
|
614
|
+
if n_i < n_t:
|
|
615
|
+
return r
|
|
616
|
+
|
|
617
|
+
return 1 - (1 - r) / m2
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def diffuse_glass_R(n_air, n_slide, n_slab):
|
|
621
|
+
"""Calculate the total diffuse specular reflection for air-glass-tissue interface.
|
|
622
|
+
|
|
623
|
+
This function computes the total diffuse specular reflection from the interface
|
|
624
|
+
between air, a glass slide, and tissue. It utilizes the Fresnel reflection coefficients
|
|
625
|
+
for the air-glass and glass-tissue interfaces to computes the total diffuse reflection.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
n_air: The refractive index of the surrounding air.
|
|
629
|
+
n_slide: The refractive index of the glass slide.
|
|
630
|
+
n_slab: The refractive index of the tissue slab.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
The total diffuse specular reflection coefficient.
|
|
634
|
+
"""
|
|
635
|
+
r_airglass = R1(n_air, n_slide)
|
|
636
|
+
r_glasstissue = R1(n_slide, n_slab)
|
|
637
|
+
r_temp = r_airglass * r_glasstissue
|
|
638
|
+
|
|
639
|
+
if r_temp >= 1:
|
|
640
|
+
return 1.0
|
|
641
|
+
|
|
642
|
+
return (r_airglass + r_glasstissue - 2 * r_temp) / (1 - r_temp)
|