skinoptics 0.0.1b8__py3-none-any.whl → 0.0.2__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.
@@ -1,955 +1,954 @@
1
- '''
2
- | SkinOptics
3
- | Copyright (C) 2024-2025 Victor Lima
4
-
5
- | This program is free software: you can redistribute it and/or modify
6
- | it under the terms of the GNU General Public License as published by
7
- | the Free Software Foundation, either version 3 of the License, or
8
- | (at your option) any later version.
9
-
10
- | This program is distributed in the hope that it will be useful,
11
- | but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- | GNU General Public License for more details.
14
-
15
- | You should have received a copy of the GNU General Public License
16
- | along with this program. If not, see <https://www.gnu.org/licenses/>.
17
-
18
- | Victor Lima
19
- | victorporto\@ifsc.usp.br
20
- | victor.lima\@ufscar.br
21
- | victorportog.github.io
22
-
23
- | Release date:
24
- | October 2024
25
- | Last modification:
26
- | March 2025
27
-
28
- | References:
29
-
30
- | [HG41] Henyey & Greenstein 1941.
31
- | Diffuse radiation in the Galaxy.
32
- | https://doi.org/10.1086/144246
33
-
34
- | [RM80] Reynolds & McCormick 1980.
35
- | Approximate two-parameter phase function for light scattering.
36
- | https://doi.org/10.1364/JOSA.70.001206
37
-
38
- | [Bv84] Bruls & van der Leun 1984.
39
- | Forward scattering properties of human epidermal layers.
40
- | https://doi.org/10.1111/j.1751-1097.1984.tb04581.x
41
-
42
- | [JAP87] Jacques, Alter & Prahl 1987.
43
- | Angular Dependence of HeNe Laser Light Scattering by Human Dermis.
44
- | https://omlc.org/~prahl/pubs/pdfx/jacques87b.pdf
45
-
46
- | [Y*87] Yoon, Welch, Motamedi & van Gemert 1987.
47
- | Development and Application of Three-Dimensional Light Distribution Model for Laser Irradiated Tissue.
48
- | https://doi.org/10.1109/JQE.1987.1073224
49
-
50
- | [v*89] van Gemert, Jacques, Sterenborg & Star 1989.
51
- | Skin Optics.
52
- | https://doi.org/10.1109/10.42108
53
-
54
- | [CS92] Cornette & Shanks 1992.
55
- | Physically reasonable analytic expression for the single-scattering phase function
56
- | https://doi.org/10.1364/AO.31.003152
57
-
58
- | [WJ92] Wang & Jacques 1992.
59
- | Monte Carlo Modeling of Light Transport in Multi-layered Tissues in Standard C.
60
- | https://omlc.org/software/mc/mcml/MCman.pdf
61
-
62
- | [D03] Draine 2003.
63
- | Scattering by Interstellar Dust Grains. I. Optical and Ultraviolet.
64
- | https://doi.org/10.1086/379118
65
-
66
- | [F11] Frisvad 2011.
67
- | Importance sampling the Rayleigh phase function.
68
- | https://doi.org/10.1364/JOSAA.28.002436
69
-
70
- | [B*14] Bosschaart, Edelman, Aalders, van Leeuwen & Faber 2014.
71
- | A literature review and novel theoretical approach on the optical properties of whole blood.
72
- | https://doi.org/10.1007/s10103-013-1446-7
73
-
74
- | [BCK22] Baes, Camps & Kapoor 2022.
75
- | A new analytical scattering phase function for interstellar dust.
76
- | https://doi.org/10.1051/0004-6361/202142437
77
-
78
- | [JM23] Jacques & McCormick 2023.
79
- | Two-term scattering phase function for photon transport to model subdiffuse reflectance
80
- | in superficial tissues.
81
- | https://doi.org/10.1364/BOE.476461
82
- '''
83
-
84
- import numpy as np
85
- from scipy.interpolate import interp1d
86
-
87
- from skinoptics.utils import *
88
- from skinoptics.dataframes import *
89
-
90
- def ptheta_R(theta):
91
- r'''
92
- | The Rayleigh scattering phase function.
93
- | For details please check Frisvad 2011 [F11].
94
-
95
- :math:`p_{R}(\theta) = \frac{3}{8}(1 + \cos^2\theta)`
96
-
97
- :param theta: scattering angle [degrees]
98
- :type theta: float or np.ndarray
99
-
100
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
101
- '''
102
-
103
- return (3/8)*(1 + np.cos(theta*np.pi/180)**2)
104
-
105
- def ptheta_HG(theta, g):
106
- r'''
107
- | The Henyey-Greenstein scattering phase function.
108
- | For details please check Henyey & Greenstein 1941 [HG41].
109
-
110
- :math:`p_{HG}(\theta, g) = \frac{1}{2}\frac{1 - g^2}{(1 + g^2 - 2g \cos \theta )^{3/2}}`
111
-
112
- In this particular model :math:`g` is the anisotropy factor.
113
-
114
- :param theta: scattering angle [degrees]
115
- :type theta: float or np.ndarray
116
-
117
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
118
- :type g: float
119
-
120
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
121
- '''
122
-
123
- if g < -1 or g > 1:
124
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
125
- raise Exception(msg)
126
-
127
- return (1/2)*(1 - g**2)/(1 + g**2 - 2*g*np.cos(theta*np.pi/180))**(3/2)
128
-
129
- def ptheta_HGIT(theta, g, gamma):
130
- r'''
131
- | The Henyey-Greenstein scattering phase function with an isotropic term.
132
- | For details please check Jacques, Alter & Prahl 1987 [JAP87] and Yoon et al. [Y*87].
133
-
134
- :math:`p_{HGIT}(\theta, g, \gamma) = \frac{1}{2}\gamma + (1-\gamma) \mbox{ } p_{HG}(\theta, g)`
135
-
136
- In this model :math:`g` is NOT the anisotropy factor.
137
-
138
- :param theta: scattering angle [degrees]
139
- :type theta: float or np.ndarray
140
-
141
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
142
- :type g: float
143
-
144
- :param gamma: relative weight of the isotropic term component [-] (must be in the range [0, 1])
145
- :type gamma: float
146
-
147
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
148
- '''
149
-
150
- if g < -1 or g > 1:
151
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
152
- raise Exception(msg)
153
- if gamma < 0 or gamma > 1:
154
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
155
- raise Exception(msg)
156
-
157
- return (1/2)*gamma + (1 - gamma)*ptheta_HG(theta = theta, g = g)
158
-
159
- def ptheta_TTHG(theta, g1, g2, gamma):
160
- r'''
161
- | The two-term Henyey-Greenstein scattering phase function.
162
- | For details please check Baes, Camps & Kapoor 2022 [BCK22].
163
-
164
- :math:`p_{TTHG}(\theta, g_1, g_2, \gamma) = \gamma \mbox{ } p_{HG}(\theta, g_1) + (1 - \gamma) \mbox{ } p_{HG}(\theta, g_2)`
165
-
166
- :math:`g_1` characterizes the shape and the strength of the forward scattering peak
167
-
168
- :math:`g_2` characterizes the shape and the strength of the backward scattering peak
169
-
170
- :param theta: scattering angle [degrees]
171
- :type theta: float or np.ndarray
172
-
173
- :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
174
- :type g1: float
175
-
176
- :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
177
- :type g2: float
178
-
179
- :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
180
- :type gamma: float
181
-
182
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
183
- '''
184
-
185
- if g1 < 0 or g1 > 1:
186
- msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
187
- raise Exception(msg)
188
- if g2 < -1 or g2 > 0:
189
- msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
190
- raise Exception(msg)
191
- if gamma < 0 or gamma > 1:
192
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
193
- raise Exception(msg)
194
-
195
- return gamma*ptheta_HG(theta = theta, g = g1) + (1 - gamma)*ptheta_HG(theta = theta, g = g2)
196
-
197
- def ptheta_RM(theta, g, alpha):
198
- r'''
199
- | The Reynolds-McCormick scattering phase function.
200
- | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
201
-
202
- :math:`p_{RM}(\theta, g, \alpha) = 2 \frac{\alpha g}{(1 + g)^{2\alpha} - (1 - g)^{2\alpha}}\frac{(1 - g^2)^{2\alpha}}{(1 + g^2 - 2g\cos\theta)^{\alpha + 1}}`
203
-
204
-
205
- | For :math:`\alpha = 1/2` it reduces to the Henyey-Greenstein scattering phase function.
206
- | For :math:`\alpha = 1` it reduces to the Ultraspherical-2 scattering phase function.
207
-
208
- | In this model :math:`g` is the anisotropy factor only when :math:`\alpha = 1/2`.
209
-
210
- :param theta: scattering angle [degrees]
211
- :type theta: float or np.ndarray
212
-
213
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
214
- :type g: float
215
-
216
- :param alpha: parameter :math:`\alpha` [-] (must be greater than -0.5)
217
- :type alpha: float
218
-
219
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
220
- '''
221
-
222
- if g < -1 or g > 1:
223
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
224
- raise Exception(msg)
225
- if alpha <= -1/2:
226
- msg = 'The input alpha = {} is not greater than -1/2.'.format(alpha)
227
- raise Exception(msg)
228
-
229
- return 2*(alpha*g)/((1 + g)**(2*alpha) - (1 - g)**(2*alpha))*((1 - g**2)**(2*alpha))/((1 + g**2 - 2*g*np.cos(theta*np.pi/180))**(alpha + 1))
230
-
231
- def ptheta_TTRM(theta, g1, g2, alpha1, alpha2, gamma):
232
- r'''
233
- | The two-term Reynolds-McCormick scattering phase function.
234
- | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
235
-
236
- :math:`p_{TTRM}(\theta, g_1, g_2, \alpha_1, \alpha_2, \gamma) = \gamma \mbox{ } p_{RM}(\theta, g_1, \alpha_1) + (1 - \gamma) \mbox{ } p_{RM}(\theta, g_2, \alpha_2)`
237
-
238
- :math:`g_1` characterizes the shape and the strength of the forward scattering peak
239
-
240
- :math:`g_2` characterizes the shape and the strength of the backward scattering peak
241
-
242
- :param theta: scattering angle [degrees]
243
- :type theta: float or np.ndarray
244
-
245
- :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
246
- :type g1: float
247
-
248
- :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
249
- :type g2: float
250
-
251
- :param alpha1: parameter :math:`\alpha_1` [-] (must be greater than -1/2)
252
- :type alpha1: float
253
-
254
- :param alpha2: parameter :math:`\alpha_2` [-] (must be greater than -1/2)
255
- :type alpha2: float
256
-
257
- :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
258
- :type gamma: float
259
-
260
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
261
- '''
262
-
263
- if g1 < 0 or g1 > 1:
264
- msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
265
- raise Exception(msg)
266
- if g2 < -1 or g2 > 0:
267
- msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
268
- raise Exception(msg)
269
- if alpha1 <= -1/2:
270
- msg = 'The input alpha1 = {} is not greater than -1/2.'.format(alpha1)
271
- raise Exception(msg)
272
- if alpha2 <= -1/2:
273
- msg = 'The input alpha2 = {} is not greater than -1/2.'.format(alpha2)
274
- raise Exception(msg)
275
- if gamma < 0 or gamma > 1:
276
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
277
- raise Exception(msg)
278
-
279
- return gamma*ptheta_RM(theta = theta, g = g1, alpha = alpha1) + (1 - gamma)*ptheta_RM(theta = theta, g = g2, alpha = alpha2)
280
-
281
- def ptheta_CS(theta, g):
282
- r'''
283
- | The Cornette-Shanks scattering phase function.
284
- | For details please check Cornette & Shanks 1992 [CS92].
285
-
286
- :math:`p_{CS}(\theta, g) = \frac{3}{2}\frac{1 + \cos^2\theta}{2 + g^2} \mbox{ } p_{HG}(\theta, g)`
287
-
288
- | In this model :math:`g` is NOT the anisotropy factor.
289
-
290
- :param theta: scattering angle [degrees]
291
- :type theta: float or np.ndarray
292
-
293
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
294
- :type g: float
295
-
296
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
297
- '''
298
-
299
- if g < -1 or g > 1:
300
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
301
- raise Exception(msg)
302
-
303
- return (3/2)*(1 + np.cos(theta*np.pi/180.)**2)/(2 + g**2)*ptheta_HG(theta = theta, g = g)
304
-
305
- def ptheta_D(theta, g, alpha):
306
- r'''
307
- | The Draine scattering phase function.
308
- | For details please check Draine 2003 [D03].
309
-
310
- :math:`p_{D}(\theta, g, \alpha) = 3\frac{1 + \alpha \cos^2\theta}{3 + \alpha (1 + 2g^2)} \mbox{ } p_{HG}(\theta, g)`
311
-
312
- | For :math:`\alpha = 1` and :math:`g = 0` it reduces to the Rayleigh scattering phase function.
313
- | For :math:`\alpha = 0` it reduces to the Henyey-Greenstein scattering phase function.
314
- | For :math:`\alpha = 1` it reduces to the Cornette-Shanks scattering phase function
315
-
316
- | In this model :math:`g` is NOT the anisotropy factor.
317
-
318
- :param theta: scattering angle [degrees]
319
- :type theta: float or np.ndarray
320
-
321
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
322
- :type g: float
323
-
324
- :param alpha: parameter :math:`\alpha` [-]
325
- :type alpha: float
326
-
327
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
328
- '''
329
-
330
- if g < -1 or g > 1:
331
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
332
- raise Exception(msg)
333
-
334
- return 3*(1 + alpha*np.cos(theta*np.pi/180.)**2)/(3 + alpha*(1 + 2*g**2))*ptheta_HG(theta = theta, g = g)
335
-
336
- def ptheta_U2(theta, g):
337
- r'''
338
- | The Ultraspherical-2 scattering phase function.
339
- | For details please check Baes, Camps & Kapoor 2022 [BCK22].
340
-
341
- :math:`p_{U2}(\theta, g) = \frac{1}{2}\frac{(1 - g^2)^2}{(1 + g^2 - 2g \cos \theta)^2}`
342
-
343
- | In this model :math:`g` is NOT the anisotropy factor.
344
-
345
- :param theta: scattering angle [degrees]
346
- :type theta: float or np.ndarray
347
-
348
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
349
- :type g: float
350
-
351
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
352
- '''
353
-
354
- if g < -1 or g > 1:
355
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
356
- raise Exception(msg)
357
-
358
- return (1/2)*(1 - g**2)**2/(1 + g**2 - 2*g*np.cos(theta*np.pi/180.))**2
359
-
360
- def ptheta_TTU2(theta, g1, g2, gamma):
361
- r'''
362
- | The two-term Ultraspherical-2 scattering phase function.
363
- | For details please check Baes, Camps & Kapoor 2022 [BCK22].
364
-
365
- :math:`p_{TTU2}(\theta, g_1, g_2, \gamma) = \gamma \mbox{ } p_{U2}(\theta, g_1) + (1 - \gamma) \mbox{ } p_{U2}(\theta, g_2)`
366
-
367
- :math:`g_1` characterizes the shape and the strength of the forward scattering peak
368
-
369
- :math:`g_2` characterizes the shape and the strength of the backward scattering peak
370
-
371
- :param theta: scattering angle [degrees]
372
- :type theta: float or np.ndarray
373
-
374
- :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
375
- :type g1: float
376
-
377
- :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
378
- :type g2: float
379
-
380
- :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
381
- :type gamma: float
382
-
383
- :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
384
- '''
385
-
386
- if g1 < 0 or g1 > 1:
387
- msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
388
- raise Exception(msg)
389
- if g2 < -1 or g2 > 0:
390
- msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
391
- raise Exception(msg)
392
- if gamma < 0 or gamma > 1:
393
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
394
- raise Exception(msg)
395
-
396
- return gamma*ptheta_U2(theta = theta, g = g1) + (1 - gamma)*ptheta_U2(theta = theta, g = g2)
397
-
398
- def theta_R_from_RND(n_RND = int(1E6), seed_RND = None):
399
- r'''
400
- | The scattering angle distribution as a function of a set of random numbers uniformly
401
- | distributed over the interval [0, 1), assuming the Rayleigh scattering phase function.
402
- | For details please check section 3.B from Frisvad 2011 [F11].
403
-
404
- | :math:`\theta_{R} = \mbox{arccos}(\sqrt[3]{u + v} + \sqrt[3]{u - v})`
405
- | with
406
- | :math:`u = -2(2 \xi - 1)`
407
- | :math:`v = \sqrt{4(2 \xi - 1)^2 + 1}`
408
- | in which :math:`\xi` is a random number in the interval [0, 1)
409
-
410
- :param n_RND: number of random numbers [-] (default to int(1E6))
411
- :type n_RND: int
412
-
413
- :param seed_RND: seed for the random number generator np.random.default_rng() [-] (default to None)
414
- :type seed_RND: int
415
-
416
- :return: - **theta** (*np.ndarray*) – scattering angle [degrees]
417
- '''
418
-
419
- rng = np.random.default_rng(seed = seed_RND)
420
- xi = rng.uniform(0., 1., size = n_RND)
421
- two_times_xi_minus_one = 2*xi - 1
422
- term1 = -2*two_times_xi_minus_one
423
- term2 = np.sqrt(4*two_times_xi_minus_one**2 + 1)
424
-
425
- return np.arccos(np.cbrt(term1 + term2) + np.cbrt(term1 - term2))*180/np.pi
426
-
427
-
428
- def theta_HG_from_RND(g, n_RND = int(1E6), seed_RND = None):
429
- r'''
430
- | The scattering angle distribution as a function of the anisotropy factor and a set of random
431
- | numbers uniformly distributed over the interval [0, 1), assuming the Henyey-Greenstein
432
- | scattering phase function.
433
- | For details please check section 3.5 from Wang & Jacques 1992 [WJ92].
434
-
435
- :math:`\theta_{HG} =
436
- \left \{ \begin{matrix}
437
- \mbox{arccos}(2 \xi - 1) , & \mbox{if } g = 0 \\
438
- \mbox{arccos}\left\{\frac{1}{2g} \left[1 + g^2 - \left(\frac{1 - g^2}{1 - g + 2g \xi}\right)^2\right]\right\}, & \mbox{if } g \ne 0
439
- \end{matrix} \right.`
440
-
441
- in which :math:`\xi` is a random number in the interval [0, 1)
442
-
443
- In this particular model :math:`g` is the anisotropy factor.
444
-
445
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
446
- :type g: float
447
-
448
- :param n_RND: number of random numbers [-] (default to int(1E6))
449
- :type n_RND: int
450
-
451
- :param seed_RND: seed for the random number generator np.random.default_rng() [-] (default to None)
452
- :type seed_RND: int
453
-
454
- :return: - **theta** (*np.ndarray*) – scattering angle [degrees]
455
- '''
456
-
457
- if g < -1 or g > 1:
458
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
459
- raise Exception(msg)
460
-
461
- rng = np.random.default_rng(seed = seed_RND)
462
- xi = rng.uniform(0., 1., size = n_RND)
463
- if g == 0:
464
- theta_HG = np.arccos(2*xi - 1)*180./np.pi
465
- else:
466
- theta_HG = np.arccos(1/(2*g)*(1 + g**2 - ((1 - g**2)/(1 - g + 2*g*xi))**2))*180./np.pi
467
-
468
- return theta_HG
469
-
470
- def theta_U2_from_RND(g, n_RND = int(1E6)):
471
- r'''
472
- | The scattering angle distribution as a function of the g parameter and a set of random
473
- | numbers uniformly distributed over the interval [0, 1), assuming the Ultraspherical-2
474
- | scattering phase function.
475
- | For details please check section 4.4.2 from Baes, Camps & Kapoor 2022 [BCK22].
476
-
477
- :math:`\theta_{U2} = arccos\left[\frac{(1 + g)^2 - 2 \xi (1 + g^2)}{(1 + g)^2 - 4g \xi}\right]`
478
-
479
- in which :math:`\xi` is a random number in the interval [0, 1)
480
-
481
- | In this model :math:`g` is NOT the anisotropy factor.
482
-
483
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
484
- :type g: float
485
-
486
- :param n_RND: number of random numbers [-] (default to int(1E6))
487
- :type n_RND: int
488
-
489
- :param seed_RND: seed for the random number generator np.random.default_rng() [-] (default to None)
490
- :type seed_RND: int
491
-
492
- :return: - **theta** (*np.ndarray*) – scattering angle [degrees]
493
- '''
494
-
495
- rng = np.random.default_rng(seed = seed_RND)
496
- xi = rng.uniform(0., 1., size = n_RND)
497
-
498
- return np.arccos(((1 + g)**2 - 2*xi*(1 + g**2))/((1 + g)**2 - 4*g*xi))*180./np.pi
499
-
500
- def costheta_HGIT(g, gamma):
501
- r'''
502
- | The anisotropy factor as a function of the parameters g and gamma, assuming the
503
- | Henyey-Greenstein scattering phase function with an isotropic term.
504
- | For details please check Jacques, Alter & Prahl 1987 [JAP87] and Yoon et al. [Y*87].
505
-
506
- :math:`\langle \cos\theta \rangle_{HGIT}(g, \gamma) = (1 - \gamma) \mbox{ } g`
507
-
508
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
509
- :type g: float
510
-
511
- :param gamma: fraction of isotropic term contribution [-] (must be in the range [0, 1])
512
- :type gamma: float
513
-
514
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
515
- '''
516
-
517
- if isinstance(g, np.ndarray) == True:
518
- if np.any(g < -1) or np.any(g > 1):
519
- msg = 'At least one element in the input g is out of the range [-1, 1].'
520
- raise Exception(msg)
521
- elif isinstance(g, (int, float)) == True:
522
- if g < -1 or g > 1:
523
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
524
- raise Exception(msg)
525
- else:
526
- msg = 'The input g must be int, float or np.ndarray.'
527
- raise Exception(msg)
528
-
529
- if isinstance(gamma, np.ndarray) == True:
530
- if np.any(gamma < 0) or np.any(gamma > 1):
531
- msg = 'At least one element in the input gamma is out of the range [0, 1].'
532
- raise Exception(msg)
533
- elif isinstance(gamma, (int, float)) == True:
534
- if gamma < 0 or gamma > 1:
535
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
536
- raise Exception(msg)
537
- else:
538
- msg = 'The input gamma must be int, float or np.ndarray.'
539
- raise Exception(msg)
540
-
541
- return (1 - gamma)*g
542
-
543
- def costheta_TTHG(g1, g2, gamma):
544
- r'''
545
- | The anisotropy factor as a function of the parameters g1, g2 and gamma, assuming the
546
- | two-term Henyey-Greenstein scattering phase function.
547
- | For details please check Baes, Camps & Kapoor 2022 [BCK22].
548
-
549
- :math:`\langle \cos\theta \rangle_{TTHG}(g_1, g_2, \gamma) = \gamma \mbox{ } g_1 + (1 - \gamma) \mbox { } g_2`
550
-
551
- :math:`g_1` characterizes the shape and the strength of the forward scattering peak
552
-
553
- :math:`g_2` characterizes the shape and the strength of the backward scattering peak
554
-
555
- :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
556
- :type g1: float
557
-
558
- :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
559
- :type g2: float
560
-
561
- :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
562
- :type gamma: float
563
-
564
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
565
- '''
566
-
567
- if isinstance(g1, np.ndarray) == True:
568
- if np.any(g1 < 0) or np.any(g1 > 1):
569
- msg = 'At least one element in the input g1 is out of the range [0, 1].'
570
- raise Exception(msg)
571
- elif isinstance(g1, (int, float)) == True:
572
- if g1 < 0 or g1 > 1:
573
- msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
574
- raise Exception(msg)
575
- else:
576
- msg = 'The input g1 must be int, float or np.ndarray.'
577
- raise Exception(msg)
578
-
579
- if isinstance(g2, np.ndarray) == True:
580
- if np.any(g2 < -1) or np.any(g2 > 0):
581
- msg = 'At least one element in the input g2 is out of the range [-1, 0].'
582
- raise Exception(msg)
583
- elif isinstance(g2, (int, float)) == True:
584
- if g2 < -1 or g2 > 0:
585
- msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
586
- raise Exception(msg)
587
- else:
588
- msg = 'The input g2 must be int, float or np.ndarray.'
589
- raise Exception(msg)
590
-
591
- if isinstance(gamma, np.ndarray) == True:
592
- if np.any(gamma < 0) or np.any(gamma > 1):
593
- msg = 'At least one element in the input gamma is out of the range [0, 1].'
594
- raise Exception(msg)
595
- elif isinstance(gamma, (int, float)) == True:
596
- if gamma < 0 or gamma > 1:
597
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
598
- raise Exception(msg)
599
- else:
600
- msg = 'The input gamma must be int, float or np.ndarray.'
601
- raise Exception(msg)
602
-
603
- return gamma*g1 + (1 - gamma)*g2
604
-
605
- def costheta_RM(g, alpha):
606
- r'''
607
- | The anisotropy factor as a function of the parameters g and alpha, assuming the
608
- | Reynolds-McCormick scattering phase function.
609
- | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
610
-
611
- | :math:`\langle \cos\theta \rangle_{RM}(g, \alpha) = \frac{2 \alpha g L - (1+g^2)}{2g(\alpha - 1)}`
612
- | with
613
- | :math:`L = \frac{(1+g)^{2\alpha} + (1-g)^{2\alpha}}{(1+g)^{2\alpha} - (1-g)^{2\alpha}}`
614
-
615
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
616
- :type g: float
617
-
618
- :param alpha: parameter :math:`\alpha` [-] (must be greater than -0.5)
619
- :float alpha: float or np.ndarray
620
-
621
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
622
- '''
623
-
624
- if isinstance(g, np.ndarray) == True:
625
- if np.any(g < -1) or np.any(g > 1):
626
- msg = 'At least one element in the input g is out of the range [-1, 1].'
627
- raise Exception(msg)
628
- elif isinstance(g, (int, float)) == True:
629
- if g < -1 or g > 1:
630
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
631
- raise Exception(msg)
632
- else:
633
- msg = 'The input g must be int, float or np.ndarray.'
634
- raise Exception(msg)
635
-
636
- if isinstance(alpha, np.ndarray) == True:
637
- if np.any(alpha <= -1/2):
638
- msg = 'At least one element in the input alpha is not greater than -1/2.'
639
- raise Exception(msg)
640
- costheta = np.zeros((len(alpha), 5))
641
- for i in range(len(alpha)):
642
- if i == 1:
643
- if isinstance(g, np.ndarray) == True:
644
- costheta[i] = costheta_U2(g[i])
645
- else:
646
- costheta[i] = costheta_U2(g)
647
- else:
648
- if isinstance(g, np.ndarray) == True:
649
- one_plus_g = 1 + g[i]
650
- one_minus_g = 1 - g[i]
651
- two_times_alpha = 2*alpha[i]
652
- L = (one_plus_g**two_times_alpha + one_minus_g**two_times_alpha)/(one_plus_g**two_times_alpha - one_minus_g**two_times_alpha)
653
- costheta[i] = (2*alpha[i]*g[i]*L - (1 + g[i]**2))/(2*g[i]*(alpha[i] - 1))
654
- else:
655
- one_plus_g = 1 + g
656
- one_minus_g = 1 - g
657
- two_times_alpha = 2*alpha
658
- L = (one_plus_g**two_times_alpha + one_minus_g**two_times_alpha)/(one_plus_g**two_times_alpha - one_minus_g**two_times_alpha)
659
- costheta[i] = (2*alpha[i]*g*L - (1 + g**2))/(2*g*(alpha[i] - 1))
660
- elif isinstance(alpha, (int, float)) == True:
661
- if alpha <= -1/2:
662
- msg = 'The input alpha = {} is not greater than -1/2.'.format(alpha)
663
- raise Exception(msg)
664
- if alpha == 1:
665
- costheta = costheta_U2(g)
666
- else:
667
- one_plus_g = 1 + g
668
- one_minus_g = 1 - g
669
- two_times_alpha = 2*alpha
670
- L = (one_plus_g**two_times_alpha + one_minus_g**two_times_alpha)/(one_plus_g**two_times_alpha - one_minus_g**two_times_alpha)
671
- costheta = (2*alpha*g*L - (1 + g**2))/(2*g*(alpha - 1))
672
- else:
673
- msg = 'The input alpha must be int, float or np.ndarray.'
674
- raise Exception(msg)
675
-
676
- return costheta
677
-
678
- def costheta_TTRM(g1, g2, alpha1, alpha2, gamma):
679
- r'''
680
- | The anisotropy factor as a function of the parameters g1, g2, alpha1, alpha2 and gamma,
681
- | assuming the two-term Reynolds-McCormick scattering phase function.
682
- | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
683
-
684
- :math:`\langle \cos\theta \rangle_{TTRM}(g_1, g_2, \alpha_1, \alpha_2, \gamma) = \gamma \mbox{ } \langle \cos\theta \rangle_{RM}(g_1, \alpha_1) + (1 - \gamma) \mbox{ } \langle \cos\theta \rangle_{RM}(g_2, \alpha_2)`
685
-
686
- :math:`g_1` characterizes the shape and the strength of the forward scattering peak
687
-
688
- :math:`g_2` characterizes the shape and the strength of the backward scattering peak
689
-
690
- :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
691
- :type g1: float
692
-
693
- :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
694
- :type g2: float
695
-
696
- :param alpha1: parameter :math:`\alpha_1` [-] (must be greater than -0.5)
697
- :type alpha1: float
698
-
699
- :param alpha2: parameter :math:`\alpha_2` [-] (must be greater than -0.5)
700
- :type alpha2: float
701
-
702
- :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
703
- :type gamma: float
704
-
705
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
706
- '''
707
-
708
- if isinstance(g1, np.ndarray) == True:
709
- if np.any(g1 < 0) or np.any(g1 > 1):
710
- msg = 'At least one element in the input g1 is out of the range [0, 1].'
711
- raise Exception(msg)
712
- elif isinstance(g1, (int, float)) == True:
713
- if g1 < 0 or g1 > 1:
714
- msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
715
- raise Exception(msg)
716
- else:
717
- msg = 'The input g1 must be int, float or np.ndarray.'
718
- raise Exception(msg)
719
-
720
- if isinstance(g2, np.ndarray) == True:
721
- if np.any(g2 < -1) or np.any(g2 > 0):
722
- msg = 'At least one element in the input g2 is out of the range [-1, 0].'
723
- raise Exception(msg)
724
- elif isinstance(g2, (int, float)) == True:
725
- if g2 < -1 or g2 > 0:
726
- msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
727
- raise Exception(msg)
728
- else:
729
- msg = 'The input g2 must be int, float or np.ndarray.'
730
- raise Exception(msg)
731
-
732
- if isinstance(alpha1, np.ndarray) == True:
733
- if np.any(alpha1 <= 1/2):
734
- msg = 'At least one element in the input alpha1 is not greater than 1/2.'
735
- raise Exception(msg)
736
- elif isinstance(alpha1, (int, float)) == True:
737
- if alpha1 <= 1/2:
738
- msg = 'The input alpha1 = {} is not greater than 1/2.'.format(alpha1)
739
- raise Exception(msg)
740
- else:
741
- msg = 'The input alpha1 must be int, float or np.ndarray.'
742
- raise Exception(msg)
743
-
744
- if isinstance(alpha2, np.ndarray) == True:
745
- if np.any(alpha2 <= 1/2):
746
- msg = 'At least one element in the input alpha2 is not greater than 1/2.'
747
- raise Exception(msg)
748
- elif isinstance(alpha2, (int, float)) == True:
749
- if alpha2 <= 1/2:
750
- msg = 'The input alpha2 = {} is not greater than 1/2.'.format(alpha2)
751
- raise Exception(msg)
752
- else:
753
- msg = 'The input alpha2 must be int, float or np.ndarray.'
754
- raise Exception(msg)
755
-
756
- if isinstance(gamma, np.ndarray) == True:
757
- if np.any(gamma < 0) or np.any(gamma > 1):
758
- msg = 'At least one element in the input gamma is out of the range [0, 1].'
759
- raise Exception(msg)
760
- elif isinstance(gamma, (int, float)) == True:
761
- if gamma < 0 or gamma > 1:
762
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
763
- raise Exception(msg)
764
- else:
765
- msg = 'The input gamma must be int, float or np.ndarray.'
766
- raise Exception(msg)
767
-
768
- return gamma*costheta_RM(g = g1, alpha = alpha1) + (1 - gamma)*costheta_RM(g = g2, alpha = alpha2)
769
-
770
- def costheta_CS(g):
771
- r'''
772
- | The anisotropy factor as a function of the parameter g, assuming the Cornette-Shanks
773
- | scattering phase function.
774
- | For details please check Cornette & Shanks 1992 [CS92].
775
-
776
- :math:`\langle \cos\theta \rangle_{CS}(g) = \frac{3(4 + g^2)}{5(2 + g^2)} \mbox{ } g`
777
-
778
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
779
- :type g: float
780
-
781
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
782
- '''
783
-
784
- if isinstance(g, np.ndarray) == True:
785
- if np.any(g < -1) or np.any(g > 1):
786
- msg = 'At least one element in the input g is out of the range [-1, 1].'
787
- raise Exception(msg)
788
- elif isinstance(g, (int, float)) == True:
789
- if g < -1 or g > 1:
790
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
791
- raise Exception(msg)
792
- else:
793
- msg = 'The input g must be int, float or np.ndarray.'
794
- raise Exception(msg)
795
-
796
- return g*(3*(4 + g**2))/(5*(2 + g**2))
797
-
798
- def costheta_D(g, alpha):
799
- r'''
800
- | The anisotropy factor as a function of the parameters g and alpha, assuming the Draine
801
- | scattering phase function.
802
- | For details please check Draine 2003 [D03].
803
-
804
- :math:`\langle \cos\theta \rangle_{D}(g, \alpha) = \frac{1 + \alpha(3 + 2g^2)/5}{1 + \alpha(1 + 2g^2)/3} \mbox{ } g`
805
-
806
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
807
- :type g: float
808
-
809
- :param alpha: parameter :math:`\alpha` [-]
810
- :float alpha: float or np.ndarray
811
-
812
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
813
- '''
814
-
815
- if isinstance(g, np.ndarray) == True:
816
- if np.any(g < -1) or np.any(g > 1):
817
- msg = 'At least one element in the input g is out of the range [-1, 1].'
818
- raise Exception(msg)
819
- elif isinstance(g, (int, float)) == True:
820
- if g < -1 or g > 1:
821
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
822
- raise Exception(msg)
823
- else:
824
- msg = 'The input g must be int, float or np.ndarray.'
825
- raise Exception(msg)
826
-
827
- return g*(1 + alpha*(3 + 2*g**2)/5)/(1 + alpha*(1 + 2*g**2)/3)
828
-
829
- def costheta_U2(g):
830
- r'''
831
- | The anisotropy factor as a function of the parameter g, assuming the Ultraspherical-2
832
- | scattering phase function.
833
- | For details please check Baes, Camps & Kapoor 2022 [BCK22].
834
-
835
- :math:`\langle \cos\theta \rangle_{U2}(g) = \frac{1+g^2}{2g} + \left(\frac{1-g^2}{2g}\right)^2 \mbox{ ln} \left(\frac{1-g}{1+g}\right)`
836
-
837
- :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
838
- :type g: float
839
-
840
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
841
- '''
842
-
843
- if isinstance(g, np.ndarray) == True:
844
- if np.any(g < -1) or np.any(g > 1):
845
- msg = 'At least one element in the input g is out of the range [-1, 1].'
846
- raise Exception(msg)
847
- elif isinstance(g, (int, float)) == True:
848
- if g < -1 or g > 1:
849
- msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
850
- raise Exception(msg)
851
- else:
852
- msg = 'The input g must be int, float or np.ndarray.'
853
- raise Exception(msg)
854
-
855
- return (1 + g**2)/(2*g) + ((1 - g**2)/(2*g))**2*np.log((1 - g)/(1 + g))
856
-
857
- def costheta_TTU2(g1, g2, gamma):
858
- r'''
859
- | The anisotropy factor as a function of the parameters g1, g2 and gamma, assuming the
860
- | two-term Ultraspherical-2 scattering phase function.
861
- | For details please check Baes, Camps & Kapoor 2022 [BCK22].
862
-
863
- :math:`\langle \cos\theta \rangle_{TTU2}(g_1, g_2, \gamma) = \gamma \mbox{ } \langle \cos\theta \rangle_{U2}(g_1) + (1 - \gamma) \mbox{ } \langle \cos\theta \rangle_{U2}(g_2)`
864
-
865
- :math:`g_1` characterizes the shape and the strength of the forward scattering peak
866
-
867
- :math:`g_2` characterizes the shape and the strength of the backward scattering peak
868
-
869
- :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
870
- :type g1: float
871
-
872
- :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
873
- :type g2: float
874
-
875
- :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
876
- :type gamma: float
877
-
878
- :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
879
- '''
880
-
881
- if isinstance(g1, np.ndarray) == True:
882
- if np.any(g1 < 0) or np.any(g1 > 1):
883
- msg = 'At least one element in the input g1 is out of the range [0, 1].'
884
- raise Exception(msg)
885
- elif isinstance(g1, (int, float)) == True:
886
- if g1 < 0 or g1 > 1:
887
- msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
888
- raise Exception(msg)
889
- else:
890
- msg = 'The input g1 must be int, float or np.ndarray.'
891
- raise Exception(msg)
892
-
893
- if isinstance(g2, np.ndarray) == True:
894
- if np.any(g2 < -1) or np.any(g2 > 0):
895
- msg = 'At least one element in the input g2 is out of the range [-1, 0].'
896
- raise Exception(msg)
897
- elif isinstance(g2, (int, float)) == True:
898
- if g2 < -1 or g2 > 0:
899
- msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
900
- raise Exception(msg)
901
- else:
902
- msg = 'The input g2 must be int, float or np.ndarray.'
903
- raise Exception(msg)
904
-
905
- if isinstance(gamma, np.ndarray) == True:
906
- if np.any(gamma < 0) or np.any(gamma > 1):
907
- msg = 'At least one element in the input gamma is out of the range [0, 1].'
908
- raise Exception(msg)
909
- elif isinstance(gamma, (int, float)) == True:
910
- if gamma < 0 or gamma > 1:
911
- msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
912
- raise Exception(msg)
913
- else:
914
- msg = 'The input gamma must be int, float or np.ndarray.'
915
- raise Exception(msg)
916
-
917
- return gamma*costheta_U2(g = g1) + (1 - gamma)*costheta_U2(g = g2)
918
-
919
- def g_vanGemert(lambda0):
920
- r'''
921
- | The anisotropy factor of human EPIDERMIS or DERMIS as a function of wavelength.
922
- | van Gemert et al. 1989 [v*89]'s fit for experimental data from Bruls & van der Leun 1984
923
- | [Bv84] (epidermis, 302, 365, 436 and 546 nm) and Jacques, Alter & Prahl 1987 [JAP87]
924
- | (dermis, 633 nm).
925
-
926
- :math:`g(\lambda) = 0.29 \times 10^{-3} \lambda + 0.62`
927
-
928
- | wavelength range: [302 nm, 633 nm]
929
-
930
- :param lambda0: wavelength [nm]
931
- :type lambda0: float or np.ndarray
932
-
933
- :return: - **g** (*float or np.ndarray*) – anisotropy factor [-]
934
- '''
935
-
936
- return linear(lambda0, 0.29E-3, 0.62)
937
-
938
- def g_Bosschaart(lambda0):
939
- r'''
940
- | The anisotropy factor of OXYGENATED BLOOD as a function of wavelength.
941
- | Linear interpolation of experimental data compiled by Bosschaart et al. 2014 [B*14].
942
-
943
- wavelength range: [251 nm, 1000 nm]
944
-
945
- :param lambda0: wavelength [nm]
946
- :type lambda0: float or np.ndarray
947
-
948
- :return: - **g** (*float or np.ndarray*) – anisotropy factor [-]
949
- '''
950
-
951
- return interp1d(np.array(oxy_and_deo_Bosschaart_dataframe)[:,0],
952
- np.array(oxy_and_deo_Bosschaart_dataframe)[:,4],
953
- bounds_error = False,
954
- fill_value = (np.array(oxy_and_deo_Bosschaart_dataframe)[0,4],
955
- np.array(oxy_and_deo_Bosschaart_dataframe)[-1,4]))(lambda0)
1
+ '''
2
+ | SkinOptics
3
+ | Copyright (C) 2024-2025 Victor Lima
4
+
5
+ | This program is free software: you can redistribute it and/or modify
6
+ | it under the terms of the GNU General Public License as published by
7
+ | the Free Software Foundation, either version 3 of the License, or
8
+ | (at your option) any later version.
9
+
10
+ | This program is distributed in the hope that it will be useful,
11
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ | GNU General Public License for more details.
14
+
15
+ | You should have received a copy of the GNU General Public License
16
+ | along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ | Victor Lima
19
+ | victor.lima\@ufscar.br
20
+ | victorportog.github.io
21
+
22
+ | Release date:
23
+ | October 2024
24
+ | Last modification:
25
+ | March 2025
26
+
27
+ | References:
28
+
29
+ | [HG41] Henyey & Greenstein 1941.
30
+ | Diffuse radiation in the Galaxy.
31
+ | https://doi.org/10.1086/144246
32
+
33
+ | [RM80] Reynolds & McCormick 1980.
34
+ | Approximate two-parameter phase function for light scattering.
35
+ | https://doi.org/10.1364/JOSA.70.001206
36
+
37
+ | [Bv84] Bruls & van der Leun 1984.
38
+ | Forward scattering properties of human epidermal layers.
39
+ | https://doi.org/10.1111/j.1751-1097.1984.tb04581.x
40
+
41
+ | [JAP87] Jacques, Alter & Prahl 1987.
42
+ | Angular Dependence of HeNe Laser Light Scattering by Human Dermis.
43
+ | https://omlc.org/~prahl/pubs/pdfx/jacques87b.pdf
44
+
45
+ | [Y*87] Yoon, Welch, Motamedi & van Gemert 1987.
46
+ | Development and Application of Three-Dimensional Light Distribution Model for Laser Irradiated Tissue.
47
+ | https://doi.org/10.1109/JQE.1987.1073224
48
+
49
+ | [v*89] van Gemert, Jacques, Sterenborg & Star 1989.
50
+ | Skin Optics.
51
+ | https://doi.org/10.1109/10.42108
52
+
53
+ | [CS92] Cornette & Shanks 1992.
54
+ | Physically reasonable analytic expression for the single-scattering phase function
55
+ | https://doi.org/10.1364/AO.31.003152
56
+
57
+ | [WJ92] Wang & Jacques 1992.
58
+ | Monte Carlo Modeling of Light Transport in Multi-layered Tissues in Standard C.
59
+ | https://omlc.org/software/mc/mcml/MCman.pdf
60
+
61
+ | [D03] Draine 2003.
62
+ | Scattering by Interstellar Dust Grains. I. Optical and Ultraviolet.
63
+ | https://doi.org/10.1086/379118
64
+
65
+ | [F11] Frisvad 2011.
66
+ | Importance sampling the Rayleigh phase function.
67
+ | https://doi.org/10.1364/JOSAA.28.002436
68
+
69
+ | [B*14] Bosschaart, Edelman, Aalders, van Leeuwen & Faber 2014.
70
+ | A literature review and novel theoretical approach on the optical properties of whole blood.
71
+ | https://doi.org/10.1007/s10103-013-1446-7
72
+
73
+ | [BCK22] Baes, Camps & Kapoor 2022.
74
+ | A new analytical scattering phase function for interstellar dust.
75
+ | https://doi.org/10.1051/0004-6361/202142437
76
+
77
+ | [JM23] Jacques & McCormick 2023.
78
+ | Two-term scattering phase function for photon transport to model subdiffuse reflectance
79
+ | in superficial tissues.
80
+ | https://doi.org/10.1364/BOE.476461
81
+ '''
82
+
83
+ import numpy as np
84
+ from scipy.interpolate import interp1d
85
+
86
+ from skinoptics.utils import *
87
+ from skinoptics.dataframes import *
88
+
89
+ def ptheta_R(theta):
90
+ r'''
91
+ | The Rayleigh scattering phase function.
92
+ | For details please check Frisvad 2011 [F11].
93
+
94
+ :math:`p_{R}(\theta) = \frac{3}{8}(1 + \cos^2\theta)`
95
+
96
+ :param theta: scattering angle [degrees]
97
+ :type theta: float or np.ndarray
98
+
99
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
100
+ '''
101
+
102
+ return (3/8)*(1 + np.cos(theta*np.pi/180)**2)
103
+
104
+ def ptheta_HG(theta, g):
105
+ r'''
106
+ | The Henyey-Greenstein scattering phase function.
107
+ | For details please check Henyey & Greenstein 1941 [HG41].
108
+
109
+ :math:`p_{HG}(\theta, g) = \frac{1}{2}\frac{1 - g^2}{(1 + g^2 - 2g \cos \theta )^{3/2}}`
110
+
111
+ In this particular model :math:`g` is the anisotropy factor.
112
+
113
+ :param theta: scattering angle [degrees]
114
+ :type theta: float or np.ndarray
115
+
116
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
117
+ :type g: float
118
+
119
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
120
+ '''
121
+
122
+ if g < -1 or g > 1:
123
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
124
+ raise Exception(msg)
125
+
126
+ return (1/2)*(1 - g**2)/(1 + g**2 - 2*g*np.cos(theta*np.pi/180))**(3/2)
127
+
128
+ def ptheta_HGIT(theta, g, gamma):
129
+ r'''
130
+ | The Henyey-Greenstein scattering phase function with an isotropic term.
131
+ | For details please check Jacques, Alter & Prahl 1987 [JAP87] and Yoon et al. [Y*87].
132
+
133
+ :math:`p_{HGIT}(\theta, g, \gamma) = \frac{1}{2}\gamma + (1-\gamma) \mbox{ } p_{HG}(\theta, g)`
134
+
135
+ In this model :math:`g` is NOT the anisotropy factor.
136
+
137
+ :param theta: scattering angle [degrees]
138
+ :type theta: float or np.ndarray
139
+
140
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
141
+ :type g: float
142
+
143
+ :param gamma: relative weight of the isotropic term component [-] (must be in the range [0, 1])
144
+ :type gamma: float
145
+
146
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
147
+ '''
148
+
149
+ if g < -1 or g > 1:
150
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
151
+ raise Exception(msg)
152
+ if gamma < 0 or gamma > 1:
153
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
154
+ raise Exception(msg)
155
+
156
+ return (1/2)*gamma + (1 - gamma)*ptheta_HG(theta = theta, g = g)
157
+
158
+ def ptheta_TTHG(theta, g1, g2, gamma):
159
+ r'''
160
+ | The two-term Henyey-Greenstein scattering phase function.
161
+ | For details please check Baes, Camps & Kapoor 2022 [BCK22].
162
+
163
+ :math:`p_{TTHG}(\theta, g_1, g_2, \gamma) = \gamma \mbox{ } p_{HG}(\theta, g_1) + (1 - \gamma) \mbox{ } p_{HG}(\theta, g_2)`
164
+
165
+ :math:`g_1` characterizes the shape and the strength of the forward scattering peak
166
+
167
+ :math:`g_2` characterizes the shape and the strength of the backward scattering peak
168
+
169
+ :param theta: scattering angle [degrees]
170
+ :type theta: float or np.ndarray
171
+
172
+ :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
173
+ :type g1: float
174
+
175
+ :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
176
+ :type g2: float
177
+
178
+ :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
179
+ :type gamma: float
180
+
181
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
182
+ '''
183
+
184
+ if g1 < 0 or g1 > 1:
185
+ msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
186
+ raise Exception(msg)
187
+ if g2 < -1 or g2 > 0:
188
+ msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
189
+ raise Exception(msg)
190
+ if gamma < 0 or gamma > 1:
191
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
192
+ raise Exception(msg)
193
+
194
+ return gamma*ptheta_HG(theta = theta, g = g1) + (1 - gamma)*ptheta_HG(theta = theta, g = g2)
195
+
196
+ def ptheta_RM(theta, g, alpha):
197
+ r'''
198
+ | The Reynolds-McCormick scattering phase function.
199
+ | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
200
+
201
+ :math:`p_{RM}(\theta, g, \alpha) = 2 \frac{\alpha g}{(1 + g)^{2\alpha} - (1 - g)^{2\alpha}}\frac{(1 - g^2)^{2\alpha}}{(1 + g^2 - 2g\cos\theta)^{\alpha + 1}}`
202
+
203
+
204
+ | For :math:`\alpha = 1/2` it reduces to the Henyey-Greenstein scattering phase function.
205
+ | For :math:`\alpha = 1` it reduces to the Ultraspherical-2 scattering phase function.
206
+
207
+ | In this model :math:`g` is the anisotropy factor only when :math:`\alpha = 1/2`.
208
+
209
+ :param theta: scattering angle [degrees]
210
+ :type theta: float or np.ndarray
211
+
212
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
213
+ :type g: float
214
+
215
+ :param alpha: parameter :math:`\alpha` [-] (must be greater than -0.5)
216
+ :type alpha: float
217
+
218
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
219
+ '''
220
+
221
+ if g < -1 or g > 1:
222
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
223
+ raise Exception(msg)
224
+ if alpha <= -1/2:
225
+ msg = 'The input alpha = {} is not greater than -1/2.'.format(alpha)
226
+ raise Exception(msg)
227
+
228
+ return 2*(alpha*g)/((1 + g)**(2*alpha) - (1 - g)**(2*alpha))*((1 - g**2)**(2*alpha))/((1 + g**2 - 2*g*np.cos(theta*np.pi/180))**(alpha + 1))
229
+
230
+ def ptheta_TTRM(theta, g1, g2, alpha1, alpha2, gamma):
231
+ r'''
232
+ | The two-term Reynolds-McCormick scattering phase function.
233
+ | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
234
+
235
+ :math:`p_{TTRM}(\theta, g_1, g_2, \alpha_1, \alpha_2, \gamma) = \gamma \mbox{ } p_{RM}(\theta, g_1, \alpha_1) + (1 - \gamma) \mbox{ } p_{RM}(\theta, g_2, \alpha_2)`
236
+
237
+ :math:`g_1` characterizes the shape and the strength of the forward scattering peak
238
+
239
+ :math:`g_2` characterizes the shape and the strength of the backward scattering peak
240
+
241
+ :param theta: scattering angle [degrees]
242
+ :type theta: float or np.ndarray
243
+
244
+ :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
245
+ :type g1: float
246
+
247
+ :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
248
+ :type g2: float
249
+
250
+ :param alpha1: parameter :math:`\alpha_1` [-] (must be greater than -1/2)
251
+ :type alpha1: float
252
+
253
+ :param alpha2: parameter :math:`\alpha_2` [-] (must be greater than -1/2)
254
+ :type alpha2: float
255
+
256
+ :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
257
+ :type gamma: float
258
+
259
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
260
+ '''
261
+
262
+ if g1 < 0 or g1 > 1:
263
+ msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
264
+ raise Exception(msg)
265
+ if g2 < -1 or g2 > 0:
266
+ msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
267
+ raise Exception(msg)
268
+ if alpha1 <= -1/2:
269
+ msg = 'The input alpha1 = {} is not greater than -1/2.'.format(alpha1)
270
+ raise Exception(msg)
271
+ if alpha2 <= -1/2:
272
+ msg = 'The input alpha2 = {} is not greater than -1/2.'.format(alpha2)
273
+ raise Exception(msg)
274
+ if gamma < 0 or gamma > 1:
275
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
276
+ raise Exception(msg)
277
+
278
+ return gamma*ptheta_RM(theta = theta, g = g1, alpha = alpha1) + (1 - gamma)*ptheta_RM(theta = theta, g = g2, alpha = alpha2)
279
+
280
+ def ptheta_CS(theta, g):
281
+ r'''
282
+ | The Cornette-Shanks scattering phase function.
283
+ | For details please check Cornette & Shanks 1992 [CS92].
284
+
285
+ :math:`p_{CS}(\theta, g) = \frac{3}{2}\frac{1 + \cos^2\theta}{2 + g^2} \mbox{ } p_{HG}(\theta, g)`
286
+
287
+ | In this model :math:`g` is NOT the anisotropy factor.
288
+
289
+ :param theta: scattering angle [degrees]
290
+ :type theta: float or np.ndarray
291
+
292
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
293
+ :type g: float
294
+
295
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
296
+ '''
297
+
298
+ if g < -1 or g > 1:
299
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
300
+ raise Exception(msg)
301
+
302
+ return (3/2)*(1 + np.cos(theta*np.pi/180.)**2)/(2 + g**2)*ptheta_HG(theta = theta, g = g)
303
+
304
+ def ptheta_D(theta, g, alpha):
305
+ r'''
306
+ | The Draine scattering phase function.
307
+ | For details please check Draine 2003 [D03].
308
+
309
+ :math:`p_{D}(\theta, g, \alpha) = 3\frac{1 + \alpha \cos^2\theta}{3 + \alpha (1 + 2g^2)} \mbox{ } p_{HG}(\theta, g)`
310
+
311
+ | For :math:`\alpha = 1` and :math:`g = 0` it reduces to the Rayleigh scattering phase function.
312
+ | For :math:`\alpha = 0` it reduces to the Henyey-Greenstein scattering phase function.
313
+ | For :math:`\alpha = 1` it reduces to the Cornette-Shanks scattering phase function
314
+
315
+ | In this model :math:`g` is NOT the anisotropy factor.
316
+
317
+ :param theta: scattering angle [degrees]
318
+ :type theta: float or np.ndarray
319
+
320
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
321
+ :type g: float
322
+
323
+ :param alpha: parameter :math:`\alpha` [-]
324
+ :type alpha: float
325
+
326
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
327
+ '''
328
+
329
+ if g < -1 or g > 1:
330
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
331
+ raise Exception(msg)
332
+
333
+ return 3*(1 + alpha*np.cos(theta*np.pi/180.)**2)/(3 + alpha*(1 + 2*g**2))*ptheta_HG(theta = theta, g = g)
334
+
335
+ def ptheta_U2(theta, g):
336
+ r'''
337
+ | The Ultraspherical-2 scattering phase function.
338
+ | For details please check Baes, Camps & Kapoor 2022 [BCK22].
339
+
340
+ :math:`p_{U2}(\theta, g) = \frac{1}{2}\frac{(1 - g^2)^2}{(1 + g^2 - 2g \cos \theta)^2}`
341
+
342
+ | In this model :math:`g` is NOT the anisotropy factor.
343
+
344
+ :param theta: scattering angle [degrees]
345
+ :type theta: float or np.ndarray
346
+
347
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
348
+ :type g: float
349
+
350
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
351
+ '''
352
+
353
+ if g < -1 or g > 1:
354
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
355
+ raise Exception(msg)
356
+
357
+ return (1/2)*(1 - g**2)**2/(1 + g**2 - 2*g*np.cos(theta*np.pi/180.))**2
358
+
359
+ def ptheta_TTU2(theta, g1, g2, gamma):
360
+ r'''
361
+ | The two-term Ultraspherical-2 scattering phase function.
362
+ | For details please check Baes, Camps & Kapoor 2022 [BCK22].
363
+
364
+ :math:`p_{TTU2}(\theta, g_1, g_2, \gamma) = \gamma \mbox{ } p_{U2}(\theta, g_1) + (1 - \gamma) \mbox{ } p_{U2}(\theta, g_2)`
365
+
366
+ :math:`g_1` characterizes the shape and the strength of the forward scattering peak
367
+
368
+ :math:`g_2` characterizes the shape and the strength of the backward scattering peak
369
+
370
+ :param theta: scattering angle [degrees]
371
+ :type theta: float or np.ndarray
372
+
373
+ :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
374
+ :type g1: float
375
+
376
+ :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
377
+ :type g2: float
378
+
379
+ :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
380
+ :type gamma: float
381
+
382
+ :return: - **ptheta** (*float or np.ndarray*) – scattering phase function [-]
383
+ '''
384
+
385
+ if g1 < 0 or g1 > 1:
386
+ msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
387
+ raise Exception(msg)
388
+ if g2 < -1 or g2 > 0:
389
+ msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
390
+ raise Exception(msg)
391
+ if gamma < 0 or gamma > 1:
392
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
393
+ raise Exception(msg)
394
+
395
+ return gamma*ptheta_U2(theta = theta, g = g1) + (1 - gamma)*ptheta_U2(theta = theta, g = g2)
396
+
397
+ def theta_R_from_RND(n_RND = int(1E6), seed_RND = None):
398
+ r'''
399
+ | The scattering angle distribution as a function of a set of random numbers uniformly
400
+ | distributed over the interval [0, 1), assuming the Rayleigh scattering phase function.
401
+ | For details please check section 3.B from Frisvad 2011 [F11].
402
+
403
+ | :math:`\theta_{R} = \mbox{arccos}(\sqrt[3]{u + v} + \sqrt[3]{u - v})`
404
+ | with
405
+ | :math:`u = -2(2 \xi - 1)`
406
+ | :math:`v = \sqrt{4(2 \xi - 1)^2 + 1}`
407
+ | in which :math:`\xi` is a random number in the interval [0, 1)
408
+
409
+ :param n_RND: number of random numbers [-] (default to int(1E6))
410
+ :type n_RND: int
411
+
412
+ :param seed_RND: seed for the random number generator np.random.default_rng() [-] (default to None)
413
+ :type seed_RND: int
414
+
415
+ :return: - **theta** (*np.ndarray*) – scattering angle [degrees]
416
+ '''
417
+
418
+ rng = np.random.default_rng(seed = seed_RND)
419
+ xi = rng.uniform(0., 1., size = n_RND)
420
+ two_times_xi_minus_one = 2*xi - 1
421
+ term1 = -2*two_times_xi_minus_one
422
+ term2 = np.sqrt(4*two_times_xi_minus_one**2 + 1)
423
+
424
+ return np.arccos(np.cbrt(term1 + term2) + np.cbrt(term1 - term2))*180/np.pi
425
+
426
+
427
+ def theta_HG_from_RND(g, n_RND = int(1E6), seed_RND = None):
428
+ r'''
429
+ | The scattering angle distribution as a function of the anisotropy factor and a set of random
430
+ | numbers uniformly distributed over the interval [0, 1), assuming the Henyey-Greenstein
431
+ | scattering phase function.
432
+ | For details please check section 3.5 from Wang & Jacques 1992 [WJ92].
433
+
434
+ :math:`\theta_{HG} =
435
+ \left \{ \begin{matrix}
436
+ \mbox{arccos}(2 \xi - 1) , & \mbox{if } g = 0 \\
437
+ \mbox{arccos}\left\{\frac{1}{2g} \left[1 + g^2 - \left(\frac{1 - g^2}{1 - g + 2g \xi}\right)^2\right]\right\}, & \mbox{if } g \ne 0
438
+ \end{matrix} \right.`
439
+
440
+ in which :math:`\xi` is a random number in the interval [0, 1)
441
+
442
+ In this particular model :math:`g` is the anisotropy factor.
443
+
444
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
445
+ :type g: float
446
+
447
+ :param n_RND: number of random numbers [-] (default to int(1E6))
448
+ :type n_RND: int
449
+
450
+ :param seed_RND: seed for the random number generator np.random.default_rng() [-] (default to None)
451
+ :type seed_RND: int
452
+
453
+ :return: - **theta** (*np.ndarray*) – scattering angle [degrees]
454
+ '''
455
+
456
+ if g < -1 or g > 1:
457
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
458
+ raise Exception(msg)
459
+
460
+ rng = np.random.default_rng(seed = seed_RND)
461
+ xi = rng.uniform(0., 1., size = n_RND)
462
+ if g == 0:
463
+ theta_HG = np.arccos(2*xi - 1)*180./np.pi
464
+ else:
465
+ theta_HG = np.arccos(1/(2*g)*(1 + g**2 - ((1 - g**2)/(1 - g + 2*g*xi))**2))*180./np.pi
466
+
467
+ return theta_HG
468
+
469
+ def theta_U2_from_RND(g, n_RND = int(1E6), seed_RND = None):
470
+ r'''
471
+ | The scattering angle distribution as a function of the g parameter and a set of random
472
+ | numbers uniformly distributed over the interval [0, 1), assuming the Ultraspherical-2
473
+ | scattering phase function.
474
+ | For details please check section 4.4.2 from Baes, Camps & Kapoor 2022 [BCK22].
475
+
476
+ :math:`\theta_{U2} = arccos\left[\frac{(1 + g)^2 - 2 \xi (1 + g^2)}{(1 + g)^2 - 4g \xi}\right]`
477
+
478
+ in which :math:`\xi` is a random number in the interval [0, 1)
479
+
480
+ | In this model :math:`g` is NOT the anisotropy factor.
481
+
482
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
483
+ :type g: float
484
+
485
+ :param n_RND: number of random numbers [-] (default to int(1E6))
486
+ :type n_RND: int
487
+
488
+ :param seed_RND: seed for the random number generator np.random.default_rng() [-] (default to None)
489
+ :type seed_RND: int
490
+
491
+ :return: - **theta** (*np.ndarray*) – scattering angle [degrees]
492
+ '''
493
+
494
+ rng = np.random.default_rng(seed = seed_RND)
495
+ xi = rng.uniform(0., 1., size = n_RND)
496
+
497
+ return np.arccos(((1 + g)**2 - 2*xi*(1 + g**2))/((1 + g)**2 - 4*g*xi))*180./np.pi
498
+
499
+ def costheta_HGIT(g, gamma):
500
+ r'''
501
+ | The anisotropy factor as a function of the parameters g and gamma, assuming the
502
+ | Henyey-Greenstein scattering phase function with an isotropic term.
503
+ | For details please check Jacques, Alter & Prahl 1987 [JAP87] and Yoon et al. [Y*87].
504
+
505
+ :math:`\langle \cos\theta \rangle_{HGIT}(g, \gamma) = (1 - \gamma) \mbox{ } g`
506
+
507
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
508
+ :type g: float
509
+
510
+ :param gamma: fraction of isotropic term contribution [-] (must be in the range [0, 1])
511
+ :type gamma: float
512
+
513
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
514
+ '''
515
+
516
+ if isinstance(g, np.ndarray) == True:
517
+ if np.any(g < -1) or np.any(g > 1):
518
+ msg = 'At least one element in the input g is out of the range [-1, 1].'
519
+ raise Exception(msg)
520
+ elif isinstance(g, (int, float)) == True:
521
+ if g < -1 or g > 1:
522
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
523
+ raise Exception(msg)
524
+ else:
525
+ msg = 'The input g must be int, float or np.ndarray.'
526
+ raise Exception(msg)
527
+
528
+ if isinstance(gamma, np.ndarray) == True:
529
+ if np.any(gamma < 0) or np.any(gamma > 1):
530
+ msg = 'At least one element in the input gamma is out of the range [0, 1].'
531
+ raise Exception(msg)
532
+ elif isinstance(gamma, (int, float)) == True:
533
+ if gamma < 0 or gamma > 1:
534
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
535
+ raise Exception(msg)
536
+ else:
537
+ msg = 'The input gamma must be int, float or np.ndarray.'
538
+ raise Exception(msg)
539
+
540
+ return (1 - gamma)*g
541
+
542
+ def costheta_TTHG(g1, g2, gamma):
543
+ r'''
544
+ | The anisotropy factor as a function of the parameters g1, g2 and gamma, assuming the
545
+ | two-term Henyey-Greenstein scattering phase function.
546
+ | For details please check Baes, Camps & Kapoor 2022 [BCK22].
547
+
548
+ :math:`\langle \cos\theta \rangle_{TTHG}(g_1, g_2, \gamma) = \gamma \mbox{ } g_1 + (1 - \gamma) \mbox { } g_2`
549
+
550
+ :math:`g_1` characterizes the shape and the strength of the forward scattering peak
551
+
552
+ :math:`g_2` characterizes the shape and the strength of the backward scattering peak
553
+
554
+ :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
555
+ :type g1: float
556
+
557
+ :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
558
+ :type g2: float
559
+
560
+ :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
561
+ :type gamma: float
562
+
563
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
564
+ '''
565
+
566
+ if isinstance(g1, np.ndarray) == True:
567
+ if np.any(g1 < 0) or np.any(g1 > 1):
568
+ msg = 'At least one element in the input g1 is out of the range [0, 1].'
569
+ raise Exception(msg)
570
+ elif isinstance(g1, (int, float)) == True:
571
+ if g1 < 0 or g1 > 1:
572
+ msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
573
+ raise Exception(msg)
574
+ else:
575
+ msg = 'The input g1 must be int, float or np.ndarray.'
576
+ raise Exception(msg)
577
+
578
+ if isinstance(g2, np.ndarray) == True:
579
+ if np.any(g2 < -1) or np.any(g2 > 0):
580
+ msg = 'At least one element in the input g2 is out of the range [-1, 0].'
581
+ raise Exception(msg)
582
+ elif isinstance(g2, (int, float)) == True:
583
+ if g2 < -1 or g2 > 0:
584
+ msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
585
+ raise Exception(msg)
586
+ else:
587
+ msg = 'The input g2 must be int, float or np.ndarray.'
588
+ raise Exception(msg)
589
+
590
+ if isinstance(gamma, np.ndarray) == True:
591
+ if np.any(gamma < 0) or np.any(gamma > 1):
592
+ msg = 'At least one element in the input gamma is out of the range [0, 1].'
593
+ raise Exception(msg)
594
+ elif isinstance(gamma, (int, float)) == True:
595
+ if gamma < 0 or gamma > 1:
596
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
597
+ raise Exception(msg)
598
+ else:
599
+ msg = 'The input gamma must be int, float or np.ndarray.'
600
+ raise Exception(msg)
601
+
602
+ return gamma*g1 + (1 - gamma)*g2
603
+
604
+ def costheta_RM(g, alpha):
605
+ r'''
606
+ | The anisotropy factor as a function of the parameters g and alpha, assuming the
607
+ | Reynolds-McCormick scattering phase function.
608
+ | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
609
+
610
+ | :math:`\langle \cos\theta \rangle_{RM}(g, \alpha) = \frac{2 \alpha g L - (1+g^2)}{2g(\alpha - 1)}`
611
+ | with
612
+ | :math:`L = \frac{(1+g)^{2\alpha} + (1-g)^{2\alpha}}{(1+g)^{2\alpha} - (1-g)^{2\alpha}}`
613
+
614
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
615
+ :type g: float
616
+
617
+ :param alpha: parameter :math:`\alpha` [-] (must be greater than -0.5)
618
+ :float alpha: float or np.ndarray
619
+
620
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
621
+ '''
622
+
623
+ if isinstance(g, np.ndarray) == True:
624
+ if np.any(g < -1) or np.any(g > 1):
625
+ msg = 'At least one element in the input g is out of the range [-1, 1].'
626
+ raise Exception(msg)
627
+ elif isinstance(g, (int, float)) == True:
628
+ if g < -1 or g > 1:
629
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
630
+ raise Exception(msg)
631
+ else:
632
+ msg = 'The input g must be int, float or np.ndarray.'
633
+ raise Exception(msg)
634
+
635
+ if isinstance(alpha, np.ndarray) == True:
636
+ if np.any(alpha <= -1/2):
637
+ msg = 'At least one element in the input alpha is not greater than -1/2.'
638
+ raise Exception(msg)
639
+ costheta = np.zeros((len(alpha), 5))
640
+ for i in range(len(alpha)):
641
+ if i == 1:
642
+ if isinstance(g, np.ndarray) == True:
643
+ costheta[i] = costheta_U2(g[i])
644
+ else:
645
+ costheta[i] = costheta_U2(g)
646
+ else:
647
+ if isinstance(g, np.ndarray) == True:
648
+ one_plus_g = 1 + g[i]
649
+ one_minus_g = 1 - g[i]
650
+ two_times_alpha = 2*alpha[i]
651
+ L = (one_plus_g**two_times_alpha + one_minus_g**two_times_alpha)/(one_plus_g**two_times_alpha - one_minus_g**two_times_alpha)
652
+ costheta[i] = (2*alpha[i]*g[i]*L - (1 + g[i]**2))/(2*g[i]*(alpha[i] - 1))
653
+ else:
654
+ one_plus_g = 1 + g
655
+ one_minus_g = 1 - g
656
+ two_times_alpha = 2*alpha
657
+ L = (one_plus_g**two_times_alpha + one_minus_g**two_times_alpha)/(one_plus_g**two_times_alpha - one_minus_g**two_times_alpha)
658
+ costheta[i] = (2*alpha[i]*g*L - (1 + g**2))/(2*g*(alpha[i] - 1))
659
+ elif isinstance(alpha, (int, float)) == True:
660
+ if alpha <= -1/2:
661
+ msg = 'The input alpha = {} is not greater than -1/2.'.format(alpha)
662
+ raise Exception(msg)
663
+ if alpha == 1:
664
+ costheta = costheta_U2(g)
665
+ else:
666
+ one_plus_g = 1 + g
667
+ one_minus_g = 1 - g
668
+ two_times_alpha = 2*alpha
669
+ L = (one_plus_g**two_times_alpha + one_minus_g**two_times_alpha)/(one_plus_g**two_times_alpha - one_minus_g**two_times_alpha)
670
+ costheta = (2*alpha*g*L - (1 + g**2))/(2*g*(alpha - 1))
671
+ else:
672
+ msg = 'The input alpha must be int, float or np.ndarray.'
673
+ raise Exception(msg)
674
+
675
+ return costheta
676
+
677
+ def costheta_TTRM(g1, g2, alpha1, alpha2, gamma):
678
+ r'''
679
+ | The anisotropy factor as a function of the parameters g1, g2, alpha1, alpha2 and gamma,
680
+ | assuming the two-term Reynolds-McCormick scattering phase function.
681
+ | For details please check Reynolds & McCormick 1980 [RM80] and Jacques & McCormick 2023 [JM23].
682
+
683
+ :math:`\langle \cos\theta \rangle_{TTRM}(g_1, g_2, \alpha_1, \alpha_2, \gamma) = \gamma \mbox{ } \langle \cos\theta \rangle_{RM}(g_1, \alpha_1) + (1 - \gamma) \mbox{ } \langle \cos\theta \rangle_{RM}(g_2, \alpha_2)`
684
+
685
+ :math:`g_1` characterizes the shape and the strength of the forward scattering peak
686
+
687
+ :math:`g_2` characterizes the shape and the strength of the backward scattering peak
688
+
689
+ :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
690
+ :type g1: float
691
+
692
+ :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
693
+ :type g2: float
694
+
695
+ :param alpha1: parameter :math:`\alpha_1` [-] (must be greater than -0.5)
696
+ :type alpha1: float
697
+
698
+ :param alpha2: parameter :math:`\alpha_2` [-] (must be greater than -0.5)
699
+ :type alpha2: float
700
+
701
+ :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
702
+ :type gamma: float
703
+
704
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
705
+ '''
706
+
707
+ if isinstance(g1, np.ndarray) == True:
708
+ if np.any(g1 < 0) or np.any(g1 > 1):
709
+ msg = 'At least one element in the input g1 is out of the range [0, 1].'
710
+ raise Exception(msg)
711
+ elif isinstance(g1, (int, float)) == True:
712
+ if g1 < 0 or g1 > 1:
713
+ msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
714
+ raise Exception(msg)
715
+ else:
716
+ msg = 'The input g1 must be int, float or np.ndarray.'
717
+ raise Exception(msg)
718
+
719
+ if isinstance(g2, np.ndarray) == True:
720
+ if np.any(g2 < -1) or np.any(g2 > 0):
721
+ msg = 'At least one element in the input g2 is out of the range [-1, 0].'
722
+ raise Exception(msg)
723
+ elif isinstance(g2, (int, float)) == True:
724
+ if g2 < -1 or g2 > 0:
725
+ msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
726
+ raise Exception(msg)
727
+ else:
728
+ msg = 'The input g2 must be int, float or np.ndarray.'
729
+ raise Exception(msg)
730
+
731
+ if isinstance(alpha1, np.ndarray) == True:
732
+ if np.any(alpha1 <= 1/2):
733
+ msg = 'At least one element in the input alpha1 is not greater than 1/2.'
734
+ raise Exception(msg)
735
+ elif isinstance(alpha1, (int, float)) == True:
736
+ if alpha1 <= 1/2:
737
+ msg = 'The input alpha1 = {} is not greater than 1/2.'.format(alpha1)
738
+ raise Exception(msg)
739
+ else:
740
+ msg = 'The input alpha1 must be int, float or np.ndarray.'
741
+ raise Exception(msg)
742
+
743
+ if isinstance(alpha2, np.ndarray) == True:
744
+ if np.any(alpha2 <= 1/2):
745
+ msg = 'At least one element in the input alpha2 is not greater than 1/2.'
746
+ raise Exception(msg)
747
+ elif isinstance(alpha2, (int, float)) == True:
748
+ if alpha2 <= 1/2:
749
+ msg = 'The input alpha2 = {} is not greater than 1/2.'.format(alpha2)
750
+ raise Exception(msg)
751
+ else:
752
+ msg = 'The input alpha2 must be int, float or np.ndarray.'
753
+ raise Exception(msg)
754
+
755
+ if isinstance(gamma, np.ndarray) == True:
756
+ if np.any(gamma < 0) or np.any(gamma > 1):
757
+ msg = 'At least one element in the input gamma is out of the range [0, 1].'
758
+ raise Exception(msg)
759
+ elif isinstance(gamma, (int, float)) == True:
760
+ if gamma < 0 or gamma > 1:
761
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
762
+ raise Exception(msg)
763
+ else:
764
+ msg = 'The input gamma must be int, float or np.ndarray.'
765
+ raise Exception(msg)
766
+
767
+ return gamma*costheta_RM(g = g1, alpha = alpha1) + (1 - gamma)*costheta_RM(g = g2, alpha = alpha2)
768
+
769
+ def costheta_CS(g):
770
+ r'''
771
+ | The anisotropy factor as a function of the parameter g, assuming the Cornette-Shanks
772
+ | scattering phase function.
773
+ | For details please check Cornette & Shanks 1992 [CS92].
774
+
775
+ :math:`\langle \cos\theta \rangle_{CS}(g) = \frac{3(4 + g^2)}{5(2 + g^2)} \mbox{ } g`
776
+
777
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
778
+ :type g: float
779
+
780
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
781
+ '''
782
+
783
+ if isinstance(g, np.ndarray) == True:
784
+ if np.any(g < -1) or np.any(g > 1):
785
+ msg = 'At least one element in the input g is out of the range [-1, 1].'
786
+ raise Exception(msg)
787
+ elif isinstance(g, (int, float)) == True:
788
+ if g < -1 or g > 1:
789
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
790
+ raise Exception(msg)
791
+ else:
792
+ msg = 'The input g must be int, float or np.ndarray.'
793
+ raise Exception(msg)
794
+
795
+ return g*(3*(4 + g**2))/(5*(2 + g**2))
796
+
797
+ def costheta_D(g, alpha):
798
+ r'''
799
+ | The anisotropy factor as a function of the parameters g and alpha, assuming the Draine
800
+ | scattering phase function.
801
+ | For details please check Draine 2003 [D03].
802
+
803
+ :math:`\langle \cos\theta \rangle_{D}(g, \alpha) = \frac{1 + \alpha(3 + 2g^2)/5}{1 + \alpha(1 + 2g^2)/3} \mbox{ } g`
804
+
805
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
806
+ :type g: float
807
+
808
+ :param alpha: parameter :math:`\alpha` [-]
809
+ :float alpha: float or np.ndarray
810
+
811
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
812
+ '''
813
+
814
+ if isinstance(g, np.ndarray) == True:
815
+ if np.any(g < -1) or np.any(g > 1):
816
+ msg = 'At least one element in the input g is out of the range [-1, 1].'
817
+ raise Exception(msg)
818
+ elif isinstance(g, (int, float)) == True:
819
+ if g < -1 or g > 1:
820
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
821
+ raise Exception(msg)
822
+ else:
823
+ msg = 'The input g must be int, float or np.ndarray.'
824
+ raise Exception(msg)
825
+
826
+ return g*(1 + alpha*(3 + 2*g**2)/5)/(1 + alpha*(1 + 2*g**2)/3)
827
+
828
+ def costheta_U2(g):
829
+ r'''
830
+ | The anisotropy factor as a function of the parameter g, assuming the Ultraspherical-2
831
+ | scattering phase function.
832
+ | For details please check Baes, Camps & Kapoor 2022 [BCK22].
833
+
834
+ :math:`\langle \cos\theta \rangle_{U2}(g) = \frac{1+g^2}{2g} + \left(\frac{1-g^2}{2g}\right)^2 \mbox{ ln} \left(\frac{1-g}{1+g}\right)`
835
+
836
+ :param g: parameter :math:`g` [-] (must be in the range [-1, 1])
837
+ :type g: float
838
+
839
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
840
+ '''
841
+
842
+ if isinstance(g, np.ndarray) == True:
843
+ if np.any(g < -1) or np.any(g > 1):
844
+ msg = 'At least one element in the input g is out of the range [-1, 1].'
845
+ raise Exception(msg)
846
+ elif isinstance(g, (int, float)) == True:
847
+ if g < -1 or g > 1:
848
+ msg = 'The input g = {} is out of the range [-1, 1].'.format(g)
849
+ raise Exception(msg)
850
+ else:
851
+ msg = 'The input g must be int, float or np.ndarray.'
852
+ raise Exception(msg)
853
+
854
+ return (1 + g**2)/(2*g) + ((1 - g**2)/(2*g))**2*np.log((1 - g)/(1 + g))
855
+
856
+ def costheta_TTU2(g1, g2, gamma):
857
+ r'''
858
+ | The anisotropy factor as a function of the parameters g1, g2 and gamma, assuming the
859
+ | two-term Ultraspherical-2 scattering phase function.
860
+ | For details please check Baes, Camps & Kapoor 2022 [BCK22].
861
+
862
+ :math:`\langle \cos\theta \rangle_{TTU2}(g_1, g_2, \gamma) = \gamma \mbox{ } \langle \cos\theta \rangle_{U2}(g_1) + (1 - \gamma) \mbox{ } \langle \cos\theta \rangle_{U2}(g_2)`
863
+
864
+ :math:`g_1` characterizes the shape and the strength of the forward scattering peak
865
+
866
+ :math:`g_2` characterizes the shape and the strength of the backward scattering peak
867
+
868
+ :param g1: parameter :math:`g_1` [-] (must be in the range [0, 1])
869
+ :type g1: float
870
+
871
+ :param g2: parameter :math:`g_2` [-] (must be in the range [-1, 0])
872
+ :type g2: float
873
+
874
+ :param gamma: relative weight of the forward scattering component [-] (must be in the range [0, 1])
875
+ :type gamma: float
876
+
877
+ :return: - **costheta** (*np.ndarray*) – anisotropy factor [-]
878
+ '''
879
+
880
+ if isinstance(g1, np.ndarray) == True:
881
+ if np.any(g1 < 0) or np.any(g1 > 1):
882
+ msg = 'At least one element in the input g1 is out of the range [0, 1].'
883
+ raise Exception(msg)
884
+ elif isinstance(g1, (int, float)) == True:
885
+ if g1 < 0 or g1 > 1:
886
+ msg = 'The input g1 = {} is out of the range [0, 1].'.format(g1)
887
+ raise Exception(msg)
888
+ else:
889
+ msg = 'The input g1 must be int, float or np.ndarray.'
890
+ raise Exception(msg)
891
+
892
+ if isinstance(g2, np.ndarray) == True:
893
+ if np.any(g2 < -1) or np.any(g2 > 0):
894
+ msg = 'At least one element in the input g2 is out of the range [-1, 0].'
895
+ raise Exception(msg)
896
+ elif isinstance(g2, (int, float)) == True:
897
+ if g2 < -1 or g2 > 0:
898
+ msg = 'The input g2 = {} is out of the range [-1, 0].'.format(g2)
899
+ raise Exception(msg)
900
+ else:
901
+ msg = 'The input g2 must be int, float or np.ndarray.'
902
+ raise Exception(msg)
903
+
904
+ if isinstance(gamma, np.ndarray) == True:
905
+ if np.any(gamma < 0) or np.any(gamma > 1):
906
+ msg = 'At least one element in the input gamma is out of the range [0, 1].'
907
+ raise Exception(msg)
908
+ elif isinstance(gamma, (int, float)) == True:
909
+ if gamma < 0 or gamma > 1:
910
+ msg = 'The input gamma = {} is out of the range [0, 1].'.format(gamma)
911
+ raise Exception(msg)
912
+ else:
913
+ msg = 'The input gamma must be int, float or np.ndarray.'
914
+ raise Exception(msg)
915
+
916
+ return gamma*costheta_U2(g = g1) + (1 - gamma)*costheta_U2(g = g2)
917
+
918
+ def g_vanGemert(lambda0):
919
+ r'''
920
+ | The anisotropy factor of human EPIDERMIS or DERMIS as a function of wavelength.
921
+ | van Gemert et al. 1989 [v*89]'s fit for experimental data from Bruls & van der Leun 1984
922
+ | [Bv84] (epidermis, 302, 365, 436 and 546 nm) and Jacques, Alter & Prahl 1987 [JAP87]
923
+ | (dermis, 633 nm).
924
+
925
+ :math:`g(\lambda) = 0.29 \times 10^{-3} \lambda + 0.62`
926
+
927
+ | wavelength range: [302 nm, 633 nm]
928
+
929
+ :param lambda0: wavelength [nm]
930
+ :type lambda0: float or np.ndarray
931
+
932
+ :return: - **g** (*float or np.ndarray*) – anisotropy factor [-]
933
+ '''
934
+
935
+ return linear(lambda0, 0.29E-3, 0.62)
936
+
937
+ def g_Bosschaart(lambda0):
938
+ r'''
939
+ | The anisotropy factor of OXYGENATED BLOOD as a function of wavelength.
940
+ | Linear interpolation of experimental data compiled by Bosschaart et al. 2014 [B*14].
941
+
942
+ wavelength range: [251 nm, 1000 nm]
943
+
944
+ :param lambda0: wavelength [nm]
945
+ :type lambda0: float or np.ndarray
946
+
947
+ :return: - **g** (*float or np.ndarray*) – anisotropy factor [-]
948
+ '''
949
+
950
+ return interp1d(np.array(oxy_and_deo_Bosschaart_dataframe)[:,0],
951
+ np.array(oxy_and_deo_Bosschaart_dataframe)[:,4],
952
+ bounds_error = False,
953
+ fill_value = (np.array(oxy_and_deo_Bosschaart_dataframe)[0,4],
954
+ np.array(oxy_and_deo_Bosschaart_dataframe)[-1,4]))(lambda0)