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/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)