radphen-iadpython 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iadpython/__init__.py +59 -0
- iadpython/ad.py +1377 -0
- iadpython/agrid.py +306 -0
- iadpython/cache.py +133 -0
- iadpython/combine.py +620 -0
- iadpython/constants.py +3 -0
- iadpython/data/M38597.csv +759 -0
- iadpython/double.py +376 -0
- iadpython/fresnel.py +642 -0
- iadpython/grid.py +125 -0
- iadpython/iad.py +615 -0
- iadpython/iadc.py +1003 -0
- iadpython/iadcommand.py +712 -0
- iadpython/layer.py +241 -0
- iadpython/nist.py +114 -0
- iadpython/port.py +242 -0
- iadpython/quadrature.py +172 -0
- iadpython/redistribution.py +318 -0
- iadpython/rxt.py +201 -0
- iadpython/sphere.py +582 -0
- iadpython/start.py +377 -0
- iadpython/txt.py +147 -0
- radphen_iadpython-0.7.0.dist-info/METADATA +264 -0
- radphen_iadpython-0.7.0.dist-info/RECORD +28 -0
- radphen_iadpython-0.7.0.dist-info/WHEEL +5 -0
- radphen_iadpython-0.7.0.dist-info/entry_points.txt +2 -0
- radphen_iadpython-0.7.0.dist-info/licenses/LICENSE.txt +18 -0
- radphen_iadpython-0.7.0.dist-info/top_level.txt +1 -0
iadpython/quadrature.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Module for obtaining quadrature points and weights.
|
|
2
|
+
|
|
3
|
+
Three types of gaussian quadrature are supported: normal Gaussian,
|
|
4
|
+
Radau quadrature, and Lobatto quadrature. The first method does not
|
|
5
|
+
include either endpoint of integration, Radau quadrature includes one
|
|
6
|
+
endpoint of the integration range, and Lobatto quadrature includes both
|
|
7
|
+
endpoints.
|
|
8
|
+
|
|
9
|
+
Example::
|
|
10
|
+
|
|
11
|
+
>>> import iad.quadrature
|
|
12
|
+
|
|
13
|
+
>>> n=8
|
|
14
|
+
>>> x, w = iad.quadrature.gauss(n)
|
|
15
|
+
>>> print(" i x weight")
|
|
16
|
+
>>> for i,x in enumerate(xi):
|
|
17
|
+
>>> print("%2d %+.12f %+.12f" % (i, x[i], w[i]))
|
|
18
|
+
|
|
19
|
+
>>> x, w = iad.quadrature.radau(n)
|
|
20
|
+
>>> print(" i x weight")
|
|
21
|
+
>>> for i,x in enumerate(xi):
|
|
22
|
+
>>> print("%2d %+.12f %+.12f" % (i, x[i], w[i]))
|
|
23
|
+
|
|
24
|
+
>>> x, w = iad.quadrature.lobatto(n)
|
|
25
|
+
>>> print(" i x weight")
|
|
26
|
+
>>> for i,x in enumerate(xi):
|
|
27
|
+
>>> print("%2d %+.12f %+.12f" % (i, x[i], w[i]))
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import functools
|
|
32
|
+
import scipy.special
|
|
33
|
+
import scipy.optimize
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
__all__ = (
|
|
37
|
+
"gauss",
|
|
38
|
+
"radau",
|
|
39
|
+
"lobatto",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _gauss_func(n, x):
|
|
44
|
+
"""Zeroes of this function are the Gaussian quadrature points."""
|
|
45
|
+
return scipy.special.legendre(n)(x)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _radau_func(n, x):
|
|
49
|
+
"""Zeros of this function are the Radau quadrature points."""
|
|
50
|
+
return (scipy.special.eval_legendre(n - 1, x) + scipy.special.eval_legendre(n, x)) / (1 + x)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _lobatto_func(n, x):
|
|
54
|
+
"""Zeros of this function are the Lobatto quadrature points."""
|
|
55
|
+
return scipy.special.legendre(n - 1).deriv(1)(x)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def gauss(n, a=-1, b=1):
|
|
59
|
+
"""Return abscissas and weights for Gaussian quadrature.
|
|
60
|
+
|
|
61
|
+
The definite integral ranges from a to b. The default
|
|
62
|
+
interval is -1 to 1. The quadrature approximation is
|
|
63
|
+
just the sum of :math:`w_i f(x_i)`. Neither a nor b is included
|
|
64
|
+
in the list of quadrature abscissas.
|
|
65
|
+
|
|
66
|
+
The result should be exact when integrating any polynomial
|
|
67
|
+
of degree 2n-1 or less.
|
|
68
|
+
|
|
69
|
+
If -a=b, then abscissas will be symmetric about the origin
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
n: number of quadrature points
|
|
73
|
+
a: lower limit of integral
|
|
74
|
+
b: upper limit of integral
|
|
75
|
+
Returns:
|
|
76
|
+
x: array of abscissas of length n
|
|
77
|
+
w: array of weights of length n
|
|
78
|
+
"""
|
|
79
|
+
x, w, _ = scipy.special.roots_legendre(n, mu=True)
|
|
80
|
+
|
|
81
|
+
# scale for desired interval
|
|
82
|
+
x *= 0.5 * (a - b)
|
|
83
|
+
x += 0.5 * (a + b)
|
|
84
|
+
w *= 0.5 * (b - a)
|
|
85
|
+
|
|
86
|
+
return np.flip(x), np.flip(w)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def radau(n, a=-1, b=1):
|
|
90
|
+
"""Return abscissas and weights for Radau quadrature.
|
|
91
|
+
|
|
92
|
+
The definite integral ranges from a to b. The default
|
|
93
|
+
interval is -1 to 1. The quadrature approximation is
|
|
94
|
+
just the sum of :math:`w_i f(x_i)`. The upper endpoint b is include
|
|
95
|
+
in the list of quadrature abscissas.
|
|
96
|
+
|
|
97
|
+
The result should be exact when integrating any polynomial
|
|
98
|
+
of degree 2n-2 or less.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
n: number of quadrature points
|
|
102
|
+
a: lower limit of integral
|
|
103
|
+
b: upper limit of integral
|
|
104
|
+
Returns:
|
|
105
|
+
x: array of abscissas of length n
|
|
106
|
+
w: array of weights of length n
|
|
107
|
+
"""
|
|
108
|
+
x = np.zeros(n)
|
|
109
|
+
w = np.zeros(n)
|
|
110
|
+
x[0] = -1
|
|
111
|
+
|
|
112
|
+
# the roots of P_{n} bracket the roots of P_{n}':
|
|
113
|
+
brackets = scipy.special.roots_legendre(n)[0]
|
|
114
|
+
|
|
115
|
+
f = functools.partial(_radau_func, n)
|
|
116
|
+
for i in range(n - 1):
|
|
117
|
+
x[i + 1] = scipy.optimize.brentq(f, brackets[i], brackets[i + 1])
|
|
118
|
+
|
|
119
|
+
pp = scipy.special.legendre(n - 1).deriv(1)
|
|
120
|
+
w[0] = 2 / n**2
|
|
121
|
+
w[1:] = 1 / pp(x[1:]) ** 2 / (1 - x[1:])
|
|
122
|
+
|
|
123
|
+
# scale for desired interval
|
|
124
|
+
x *= 0.5 * (a - b)
|
|
125
|
+
x += 0.5 * (b + a)
|
|
126
|
+
w *= 0.5 * (b - a)
|
|
127
|
+
|
|
128
|
+
return np.flip(x), np.flip(w)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def lobatto(n, a=-1, b=1):
|
|
132
|
+
"""Return abscissas and weights for Lobatto quadrature.
|
|
133
|
+
|
|
134
|
+
The definite integral ranges from a to b. The default
|
|
135
|
+
interval is -1 to 1. The quadrature approximation is
|
|
136
|
+
just the sum of w_i f(x_i). Both endpoints a and b are include
|
|
137
|
+
in the list of quadrature abscissas.
|
|
138
|
+
|
|
139
|
+
The result should be exact when integrating any polynomial
|
|
140
|
+
of degree 2n-3 or less.
|
|
141
|
+
|
|
142
|
+
If -a=b, then abscissas will be symmetric about the origin
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
n: number of quadrature points
|
|
146
|
+
a: lower limit of integral
|
|
147
|
+
b: upper limit of integral
|
|
148
|
+
Returns:
|
|
149
|
+
x: array of abscissas of length n
|
|
150
|
+
w: array of weights of length n
|
|
151
|
+
"""
|
|
152
|
+
x = np.zeros(n)
|
|
153
|
+
w = np.full(n, 2 / n / (n - 1))
|
|
154
|
+
x[0] = -1
|
|
155
|
+
x[-1] = 1
|
|
156
|
+
|
|
157
|
+
# The roots of P_{n-1} bracket the roots of P_{n-1}':
|
|
158
|
+
brackets = scipy.special.roots_legendre(n - 1)[0]
|
|
159
|
+
|
|
160
|
+
f = functools.partial(_lobatto_func, n)
|
|
161
|
+
for i in range(n - 2):
|
|
162
|
+
x[i + 1] = scipy.optimize.brentq(f, brackets[i], brackets[i + 1])
|
|
163
|
+
|
|
164
|
+
pp = scipy.special.legendre(n - 1)(x)
|
|
165
|
+
w[1:-1] = w[1:-1] / pp[1:-1] ** 2
|
|
166
|
+
|
|
167
|
+
# scale for desired interval
|
|
168
|
+
x *= 0.5 * (a - b)
|
|
169
|
+
x += 0.5 * (a + b)
|
|
170
|
+
w *= 0.5 * (b - a)
|
|
171
|
+
|
|
172
|
+
return np.flip(x), np.flip(w)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
r"""Module for calculating the redistribution function.
|
|
2
|
+
|
|
3
|
+
The single scattering phase function ..math:`p(\nu)` for a tissue determines the
|
|
4
|
+
amount of light scattered at an angle nu=cos(theta) from the direction of
|
|
5
|
+
incidence. The subtended angle nu is the dot product incident and exiting
|
|
6
|
+
of the unit vectors.
|
|
7
|
+
|
|
8
|
+
The redistribution function ..math:`h[i,j]` determines the fraction of light
|
|
9
|
+
scattered from an incidence cone with angle `\nu_i` into a cone with angle
|
|
10
|
+
..math:`\nu_j`. The redistribution function is calculated by averaging the phase
|
|
11
|
+
function over all possible azimuthal angles for fixed angles ..math:`\nu_i` and
|
|
12
|
+
..math:`nu_j`,
|
|
13
|
+
|
|
14
|
+
Note that the angles ..math:`\nu_i` and ..math:`\nu_j` may also be negative (light
|
|
15
|
+
travelling in the opposite direction).
|
|
16
|
+
|
|
17
|
+
When the cosine of the angle of incidence or exitance is unity (..math:`\nu_i=1` or
|
|
18
|
+
..math:`\nu_j=1`), then the redistribution function is equivalent to the phase
|
|
19
|
+
function ..math:`p(\nu_j)`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import scipy.special
|
|
23
|
+
import scipy.integrate
|
|
24
|
+
import warnings
|
|
25
|
+
import numpy as np
|
|
26
|
+
from numpy.polynomial.legendre import legvander, leggauss
|
|
27
|
+
from scipy.special import eval_legendre
|
|
28
|
+
import pandas as pd
|
|
29
|
+
from scipy.interpolate import interp1d, CubicSpline
|
|
30
|
+
|
|
31
|
+
__all__ = ('hg_elliptic',
|
|
32
|
+
'phase_legendre',
|
|
33
|
+
'legendre_coeffs_from_df')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _prepare_moments(pf_data, n_needed):
|
|
37
|
+
"""Validate and normalize a ``pf_type='MOMENTS'`` array.
|
|
38
|
+
|
|
39
|
+
Shared by ``phase_legendre`` and ``Sample._deltaM_forward_fraction`` so
|
|
40
|
+
both consumers of a supplied moments array agree on the same validation
|
|
41
|
+
and normalization convention -- rather than each re-deriving it and
|
|
42
|
+
risking drift.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
pf_data : array-like, Legendre moments a_l (index 0 = a_0)
|
|
47
|
+
n_needed : minimum number of moments required
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
a_raw : ndarray, shape (n_needed,), normalized so a_0 = 1 (matching
|
|
52
|
+
legendre_coeffs_from_df's convention; an identically-zero
|
|
53
|
+
spectrum -- matched-index / no-scattering limit -- stays zero
|
|
54
|
+
instead of raising a division-by-zero).
|
|
55
|
+
|
|
56
|
+
Raises
|
|
57
|
+
------
|
|
58
|
+
TypeError if pf_data is DataFrame-like (np.asarray would otherwise
|
|
59
|
+
silently coerce it instead of failing).
|
|
60
|
+
ValueError if pf_data is not 1-D once materialized, or has fewer than
|
|
61
|
+
n_needed entries.
|
|
62
|
+
"""
|
|
63
|
+
if hasattr(pf_data, "iloc"):
|
|
64
|
+
raise TypeError("pf_type='MOMENTS' requires a plain array, not a DataFrame")
|
|
65
|
+
a_raw = np.atleast_1d(np.asarray(pf_data, dtype=float))
|
|
66
|
+
if a_raw.ndim != 1:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"MOMENTS pf_data must be 1-D once materialized for a single "
|
|
69
|
+
"wavelength, got shape %s" % (a_raw.shape,)
|
|
70
|
+
)
|
|
71
|
+
if a_raw.size < n_needed:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"pf_type='MOMENTS' needs at least %d moments, got %d" % (n_needed, a_raw.size)
|
|
74
|
+
)
|
|
75
|
+
a_raw = a_raw[:n_needed].copy()
|
|
76
|
+
a0 = a_raw[0]
|
|
77
|
+
if np.isclose(a0, 0.0):
|
|
78
|
+
a_raw[:] = 0.0
|
|
79
|
+
else:
|
|
80
|
+
a_raw /= a0
|
|
81
|
+
return a_raw
|
|
82
|
+
|
|
83
|
+
def legendre_coeffs_from_df(
|
|
84
|
+
df, *, quad_pts=8, n_mom=None, spline_bc='not-a-knot'):
|
|
85
|
+
"""
|
|
86
|
+
Exact Legendre moments a_l (no ½(2l+1) factor) for a phase function
|
|
87
|
+
tabulated on μ = cosθ.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
df : pandas.DataFrame
|
|
92
|
+
index = μ (strictly monotonic), columns = spectra
|
|
93
|
+
quad_pts : kept only for API symmetry (default 8)
|
|
94
|
+
n_mom : highest Legendre order (default 2*quad_pts)
|
|
95
|
+
spline_bc : boundary condition passed to CubicSpline
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
a_raw : ndarray, shape (n_mom, nλ)
|
|
100
|
+
Legendre moments a_l = ∫_{-1}^1 p(μ) P_l(μ) dμ , l=0…n_mom-1
|
|
101
|
+
*normalised* so that a_0 = 1 for every spectrum.
|
|
102
|
+
"""
|
|
103
|
+
# ---------- sanity -------------------------------------------------
|
|
104
|
+
if not isinstance(df, pd.DataFrame):
|
|
105
|
+
raise TypeError("df must be a pandas DataFrame.")
|
|
106
|
+
|
|
107
|
+
df = df.sort_index() # ascending μ for CubicSpline
|
|
108
|
+
mu = df.index.to_numpy() # ascending version
|
|
109
|
+
p = df.to_numpy().T # shape (nλ, Nμ)
|
|
110
|
+
|
|
111
|
+
if n_mom is None:
|
|
112
|
+
n_mom = 2 * quad_pts + 1 # Wiscombe default
|
|
113
|
+
|
|
114
|
+
nλ = p.shape[0]
|
|
115
|
+
|
|
116
|
+
# Adaptive quadrature: use fewer points for smaller n_mom
|
|
117
|
+
nquad = max(32, min(128, 4 * n_mom))
|
|
118
|
+
g_x, g_w = leggauss(nquad)
|
|
119
|
+
|
|
120
|
+
# Evaluate Legendre polynomials once at quadrature nodes
|
|
121
|
+
P_all = np.array([eval_legendre(l, g_x) for l in range(n_mom)]) # (n_mom, nquad)
|
|
122
|
+
|
|
123
|
+
# Vectorized spline evaluation: build all splines and evaluate at once
|
|
124
|
+
vals_all = np.zeros((nquad, nλ))
|
|
125
|
+
for col, y in enumerate(p):
|
|
126
|
+
f = CubicSpline(mu, y, bc_type=spline_bc)
|
|
127
|
+
vals_all[:, col] = f(g_x)
|
|
128
|
+
|
|
129
|
+
# Vectorized integration: compute all moments for all spectra at once
|
|
130
|
+
a_raw = P_all @ (vals_all * g_w[:, None]) # (n_mom, nquad) @ (nquad, nλ) = (n_mom, nλ)
|
|
131
|
+
|
|
132
|
+
# Normalize so a_0 = 1 for each spectrum. When the supplied phase
|
|
133
|
+
# function is identically zero (matched-index / no-scattering limits),
|
|
134
|
+
# leave all moments at zero instead of creating NaNs.
|
|
135
|
+
a0 = a_raw[0, :]
|
|
136
|
+
zero_mask = np.isclose(a0, 0.0)
|
|
137
|
+
if np.any(~zero_mask):
|
|
138
|
+
a_raw[:, ~zero_mask] /= a0[~zero_mask]
|
|
139
|
+
if np.any(zero_mask):
|
|
140
|
+
a_raw[:, zero_mask] = 0.0
|
|
141
|
+
|
|
142
|
+
return a_raw
|
|
143
|
+
|
|
144
|
+
# ----------------------------------------------------------------------
|
|
145
|
+
# unified HG / tabulated redistribution via Legendre polynomials
|
|
146
|
+
# ----------------------------------------------------------------------
|
|
147
|
+
def phase_legendre(sample, *, deltam=True):
|
|
148
|
+
"""
|
|
149
|
+
Build (hp, hm) single-scatter redistribution blocks.
|
|
150
|
+
|
|
151
|
+
Modes
|
|
152
|
+
-----
|
|
153
|
+
* pf_type == 'HG' : analytic Henyey–Greenstein (fast)
|
|
154
|
+
* pf_type == 'TABULATED' : DataFrame in sample.pf_data
|
|
155
|
+
→ Legendre coeffs via helper above
|
|
156
|
+
* pf_type == 'MOMENTS' : Legendre moments a_l supplied directly in
|
|
157
|
+
sample.pf_data (bypasses the spline+
|
|
158
|
+
quadrature TABULATED needs -- for callers
|
|
159
|
+
that can evaluate their phase function
|
|
160
|
+
exactly at arbitrary angles and integrate
|
|
161
|
+
it themselves, e.g. via Gauss-Legendre
|
|
162
|
+
quadrature on mu=cos(theta))
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
sample : iadpython.Sample (must have quad_pts, nu, etc.)
|
|
166
|
+
deltam : bool apply δ-M forward-spike split
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
hp, hm : ndarray (N × N)
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
if sample.nu is None:
|
|
174
|
+
sample.update_quadrature()
|
|
175
|
+
|
|
176
|
+
mu = sample.nu
|
|
177
|
+
n = sample.quad_pts
|
|
178
|
+
P = legvander(mu, 2*n).T # (2N+1, N)
|
|
179
|
+
w = sample.twonuw / (2*mu)
|
|
180
|
+
|
|
181
|
+
pf_type = getattr(sample, "pf_type", "HG").upper()
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------- HG fast path
|
|
184
|
+
if pf_type == "HG":
|
|
185
|
+
g = float(sample.g)
|
|
186
|
+
if g == 0.0:
|
|
187
|
+
hp = hm = np.ones((n, n))
|
|
188
|
+
return hp, hm
|
|
189
|
+
a_raw = g ** np.arange(2*n+1) # analytic a_ℓ = g^ℓ
|
|
190
|
+
|
|
191
|
+
# -------------------------------------------- Custom tabulated path
|
|
192
|
+
elif pf_type == "TABULATED":
|
|
193
|
+
df = sample.pf_data
|
|
194
|
+
a_raw = legendre_coeffs_from_df(df,
|
|
195
|
+
quad_pts=n,
|
|
196
|
+
n_mom=2*n+1) # (2N+1 , nλ=1)
|
|
197
|
+
a_raw = a_raw.squeeze() # 1-D
|
|
198
|
+
|
|
199
|
+
# ----------------------------------------------- Direct moments path
|
|
200
|
+
elif pf_type == "MOMENTS":
|
|
201
|
+
a_raw = _prepare_moments(sample.pf_data, 2*n+1)
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError(f"Unknown pf_type '{pf_type}'")
|
|
205
|
+
|
|
206
|
+
# -------------------------------------------- optional δ-M trunc
|
|
207
|
+
if deltam:
|
|
208
|
+
f_spike_end = 1 - 1E-10
|
|
209
|
+
f_spike = float(np.clip(a_raw[n], 0.0, f_spike_end)) # order-n moment; matches Sample._deltaM_forward_fraction (HG: g**n)
|
|
210
|
+
a_use = (a_raw[:n] - f_spike) / (1.0 - f_spike)
|
|
211
|
+
Lmax = n
|
|
212
|
+
P_use = P[:Lmax]
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
a_use = a_raw[:2*n+1]
|
|
216
|
+
Lmax = 2*n+1
|
|
217
|
+
P_use = P[:Lmax]
|
|
218
|
+
|
|
219
|
+
chi = (2*np.arange(Lmax) + 1) * a_use # χ_ℓ weights
|
|
220
|
+
|
|
221
|
+
# -------------------------------------------- assemble blocks
|
|
222
|
+
hp = np.ones((n, n))
|
|
223
|
+
hm = np.ones((n, n))
|
|
224
|
+
|
|
225
|
+
for k in range(1, Lmax):
|
|
226
|
+
pk = P_use[k]
|
|
227
|
+
add = chi[k] * np.outer(pk, pk)
|
|
228
|
+
hp += add
|
|
229
|
+
hm += (-1)**k * add
|
|
230
|
+
return hp, hm
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def hg_elliptic(sample):
|
|
234
|
+
"""Calculate redistribution function using elliptic integrals.
|
|
235
|
+
|
|
236
|
+
This is the result of a direct integration of the Henyey-
|
|
237
|
+
Greenstein phase function.
|
|
238
|
+
|
|
239
|
+
It is not terribly useful because we cannot use the
|
|
240
|
+
delta-M method to more accurate model highly anisotropic
|
|
241
|
+
phase functions.
|
|
242
|
+
"""
|
|
243
|
+
if sample.nu is None:
|
|
244
|
+
sample.update_quadrature()
|
|
245
|
+
|
|
246
|
+
n = sample.quad_pts
|
|
247
|
+
g = sample.g**n
|
|
248
|
+
if g == 0:
|
|
249
|
+
h = np.ones([n, n])
|
|
250
|
+
return h, h
|
|
251
|
+
|
|
252
|
+
hp = np.zeros([n, n])
|
|
253
|
+
hm = np.zeros([n, n])
|
|
254
|
+
for i in range(n):
|
|
255
|
+
for j in range(i + 1):
|
|
256
|
+
ni = sample.nu[i]
|
|
257
|
+
nj = sample.nu[j]
|
|
258
|
+
gamma = 2 * g * np.sqrt(1 - ni**2) * np.sqrt(1 - nj**2)
|
|
259
|
+
|
|
260
|
+
alpha = 1 + g**2 - 2 * g * ni * nj
|
|
261
|
+
const = 2 / np.pi * (1 - g**2) / (alpha - gamma) / np.sqrt(alpha + gamma)
|
|
262
|
+
arg = np.sqrt(2 * gamma / (alpha + gamma))
|
|
263
|
+
hp[i, j] = const * scipy.special.ellipe(arg)
|
|
264
|
+
|
|
265
|
+
alpha = 1 + g**2 + 2 * g * ni * nj
|
|
266
|
+
const = 2 / np.pi * (1 - g**2) / (alpha - gamma) / np.sqrt(alpha + gamma)
|
|
267
|
+
arg = np.sqrt(2 * gamma / (alpha + gamma))
|
|
268
|
+
hm[i, j] = const * scipy.special.ellipe(arg)
|
|
269
|
+
|
|
270
|
+
# fill symmetric entries
|
|
271
|
+
for i in range(n):
|
|
272
|
+
for j in range(i + 1, n):
|
|
273
|
+
hp[i, j] = hp[j, i]
|
|
274
|
+
hm[i, j] = hm[j, i]
|
|
275
|
+
|
|
276
|
+
return hp, hm
|
|
277
|
+
|
|
278
|
+
def legendre_moments(g=None, custom_phase_function=None, max_order=16):
|
|
279
|
+
"""
|
|
280
|
+
Compute the Legendre expansion coefficients (chi_k) of a phase function.
|
|
281
|
+
|
|
282
|
+
If custom_phase_function is provided, compute chi_k via integration over [-1, 1].
|
|
283
|
+
Otherwise, use Henyey-Greenstein with parameter g.
|
|
284
|
+
If both are None, assume isotropic scattering (g=0) and issue a warning.
|
|
285
|
+
|
|
286
|
+
Arguments:
|
|
287
|
+
g: Anisotropy factor for Henyey-Greenstein (float).
|
|
288
|
+
custom_phase_function: Function of mu (float in [-1, 1]) returning P(mu).
|
|
289
|
+
max_order: Number of Legendre moments to compute (int).
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
chi: array of chi_k coefficients.
|
|
293
|
+
"""
|
|
294
|
+
chi = np.zeros(max_order)
|
|
295
|
+
|
|
296
|
+
if custom_phase_function is not None:
|
|
297
|
+
if g is not None:
|
|
298
|
+
warnings.warn("custom_phase_function is set; g will be ignored.")
|
|
299
|
+
|
|
300
|
+
for k in range(max_order):
|
|
301
|
+
integrand = lambda mu: custom_phase_function(mu) * scipy.special.legendre(k)(mu)
|
|
302
|
+
chi[k], _ = scipy.integrate.quad(integrand, -1, 1)
|
|
303
|
+
chi[k] *= (2 * k + 1) / 2
|
|
304
|
+
|
|
305
|
+
elif isinstance(g, (float, int)):
|
|
306
|
+
for k in range(max_order):
|
|
307
|
+
chi[k] = (2 * k + 1) * (g ** k - g ** max_order) / (1 - g ** max_order)
|
|
308
|
+
|
|
309
|
+
elif g is not None:
|
|
310
|
+
raise TypeError("g must be a float or int if provided.")
|
|
311
|
+
|
|
312
|
+
else:
|
|
313
|
+
warnings.warn("Neither g nor custom_phase_function provided. Proceeding with isotropic Henyey-Greenstein (g=0).")
|
|
314
|
+
g = 0
|
|
315
|
+
for k in range(max_order):
|
|
316
|
+
chi[k] = (2 * k + 1) * (g ** k - g ** max_order) / (1 - g ** max_order) if g != 1 else 0
|
|
317
|
+
|
|
318
|
+
return chi
|
iadpython/rxt.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Module for reading rxt files.
|
|
2
|
+
|
|
3
|
+
This reads an input rxt file and saves the parameters into an
|
|
4
|
+
Experiment object.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
>>> import numpy
|
|
9
|
+
>>> import matplotlib.pyplot as plt
|
|
10
|
+
>>> import iadpython
|
|
11
|
+
>>>
|
|
12
|
+
>>> filename = 'ink.rxt'
|
|
13
|
+
>>> exp = iadpython.read_rxt(filename)
|
|
14
|
+
>>> if exp.lambda0 is None:
|
|
15
|
+
>>> plt.plot(exp.m_r)
|
|
16
|
+
>>> else:
|
|
17
|
+
>>> plt.plot(exp.lambda0, exp.m_r)
|
|
18
|
+
>>> plt.ylabel("measured reflectance")
|
|
19
|
+
>>> plt.title(filename)
|
|
20
|
+
>>> plt.show()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import numpy as np
|
|
26
|
+
import iadpython
|
|
27
|
+
|
|
28
|
+
__all__ = ("read_rxt", "read_and_remove_notation")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_and_remove_notation(filename):
|
|
32
|
+
"""Read file and remove all whitespace and comments."""
|
|
33
|
+
s = ""
|
|
34
|
+
|
|
35
|
+
if not os.path.exists(filename):
|
|
36
|
+
raise ValueError('input file "%s" must end in ".rxt"' % filename)
|
|
37
|
+
|
|
38
|
+
with open(filename, encoding="utf-8") as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
line = re.sub(r"\s*#.*", "", line)
|
|
41
|
+
line = re.sub(r", ", " ", line)
|
|
42
|
+
s += line
|
|
43
|
+
|
|
44
|
+
if len(re.findall("IAD1", s)) == 0:
|
|
45
|
+
raise ValueError("Not an .rxt file. (Does not start with IAD1)")
|
|
46
|
+
|
|
47
|
+
s = re.sub(r"IAD1", "", s)
|
|
48
|
+
s = re.sub(r"\s+", " ", s)
|
|
49
|
+
s = s.rstrip()
|
|
50
|
+
s = s.lstrip()
|
|
51
|
+
return s
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fill_in_data_fixed(exp, data_in_columns):
|
|
55
|
+
"""Read data the old way by fixed column position."""
|
|
56
|
+
col = 0
|
|
57
|
+
if data_in_columns[0, 0] > 1:
|
|
58
|
+
exp.lambda0 = data_in_columns[:, 0]
|
|
59
|
+
col = 1
|
|
60
|
+
exp.m_r = data_in_columns[:, col]
|
|
61
|
+
if exp.num_measures >= 2:
|
|
62
|
+
exp.m_t = data_in_columns[:, col + 1]
|
|
63
|
+
if exp.num_measures >= 3:
|
|
64
|
+
exp.m_u = data_in_columns[:, col + 2]
|
|
65
|
+
if exp.num_measures >= 4:
|
|
66
|
+
exp.r_sphere.r_wall = data_in_columns[:, col + 3]
|
|
67
|
+
if exp.num_measures >= 5:
|
|
68
|
+
exp.t_sphere.r_wall = data_in_columns[:, col + 4]
|
|
69
|
+
if exp.num_measures >= 6:
|
|
70
|
+
exp.r_sphere.r_std = data_in_columns[:, col + 5]
|
|
71
|
+
if exp.num_measures >= 7:
|
|
72
|
+
exp.t_sphere.r_std = data_in_columns[:, col + 6]
|
|
73
|
+
if exp.num_measures > 7:
|
|
74
|
+
raise ValueError("unimplemented")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def fill_in_data_variable(exp, data_in_columns, column_letters_str):
|
|
78
|
+
"""Read data and interpret according to column_letters_str."""
|
|
79
|
+
for col, letter in enumerate(column_letters_str):
|
|
80
|
+
if letter == "a":
|
|
81
|
+
exp.default_a = data_in_columns[:, col]
|
|
82
|
+
elif letter == "b":
|
|
83
|
+
exp.default_b = data_in_columns[:, col]
|
|
84
|
+
elif letter == "B":
|
|
85
|
+
exp.d_beam = data_in_columns[:, col]
|
|
86
|
+
elif letter == "c":
|
|
87
|
+
exp.fraction_of_rc_in_mr = data_in_columns[:, col]
|
|
88
|
+
elif letter == "C":
|
|
89
|
+
exp.fraction_of_tc_in_mt = data_in_columns[:, col]
|
|
90
|
+
elif letter == "d":
|
|
91
|
+
exp.sample.d = data_in_columns[:, col]
|
|
92
|
+
elif letter == "D":
|
|
93
|
+
exp.sample.d_above = data_in_columns[:, col]
|
|
94
|
+
exp.sample.d_below = data_in_columns[:, col]
|
|
95
|
+
elif letter == "E":
|
|
96
|
+
exp.sample.b_above = data_in_columns[:, col]
|
|
97
|
+
exp.sample.b_below = data_in_columns[:, col]
|
|
98
|
+
elif letter == "e":
|
|
99
|
+
exp.error = data_in_columns[:, col]
|
|
100
|
+
elif letter == "g":
|
|
101
|
+
exp.default_g = data_in_columns[:, col]
|
|
102
|
+
elif letter == "t":
|
|
103
|
+
exp.m_t = data_in_columns[:, col]
|
|
104
|
+
elif letter == "L":
|
|
105
|
+
exp.lambda0 = data_in_columns[:, col]
|
|
106
|
+
elif letter == "n":
|
|
107
|
+
exp.n = data_in_columns[:, col]
|
|
108
|
+
elif letter == "N":
|
|
109
|
+
exp.n_above = data_in_columns[:, col]
|
|
110
|
+
exp.n_below = data_in_columns[:, col]
|
|
111
|
+
elif letter == "r":
|
|
112
|
+
exp.m_r = data_in_columns[:, col]
|
|
113
|
+
elif letter == "R":
|
|
114
|
+
exp.r_sphere.r_std = data_in_columns[:, col]
|
|
115
|
+
elif letter == "S":
|
|
116
|
+
exp.num_spheres = data_in_columns[:, col]
|
|
117
|
+
elif letter == "T":
|
|
118
|
+
exp.t_sphere.r_rstd = data_in_columns[:, col]
|
|
119
|
+
elif letter == "u":
|
|
120
|
+
exp.m_u = data_in_columns[:, col]
|
|
121
|
+
elif letter == "w":
|
|
122
|
+
exp.r_sphere.r_wall = data_in_columns[:, col]
|
|
123
|
+
elif letter == "W":
|
|
124
|
+
exp.t_sphere.r_wall = data_in_columns[:, col]
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError('unimplemented column type "%s"' % letter)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_rxt(filename):
|
|
130
|
+
"""Read an IAD input file in .rxt format.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
filename: .rxt filename
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Experiment object
|
|
137
|
+
"""
|
|
138
|
+
s = read_and_remove_notation(filename)
|
|
139
|
+
x = s.split(" ")
|
|
140
|
+
|
|
141
|
+
# Remove single-letter entries and save them
|
|
142
|
+
column_letters = [item for item in x if len(item) == 1 and item.isalpha()]
|
|
143
|
+
x = [item for item in x if not (len(item) == 1 and item.isalpha())]
|
|
144
|
+
column_letters_str = "".join(column_letters)
|
|
145
|
+
|
|
146
|
+
x = np.array([float(item) for item in x])
|
|
147
|
+
|
|
148
|
+
sample = iadpython.Sample(a=None, b=None, g=None)
|
|
149
|
+
sample.n = x[0]
|
|
150
|
+
sample.n_above = x[1]
|
|
151
|
+
sample.d = x[2]
|
|
152
|
+
sample.d_above = x[3]
|
|
153
|
+
|
|
154
|
+
# try and save people from themselves
|
|
155
|
+
if sample.d_above == 0:
|
|
156
|
+
sample.n_above = 1
|
|
157
|
+
if sample.n_above == 1:
|
|
158
|
+
sample.d_above = 0
|
|
159
|
+
if sample.n_above == 0:
|
|
160
|
+
sample.n_above = 1
|
|
161
|
+
sample.d_above = 0
|
|
162
|
+
sample.d_below = sample.d_above
|
|
163
|
+
sample.n_below = sample.n_above
|
|
164
|
+
exp = iadpython.Experiment(sample=sample)
|
|
165
|
+
|
|
166
|
+
exp.d_beam = x[4]
|
|
167
|
+
exp.rstd_r = x[5]
|
|
168
|
+
exp.num_spheres = x[6]
|
|
169
|
+
exp.method = "substitution"
|
|
170
|
+
|
|
171
|
+
if exp.num_spheres > 0:
|
|
172
|
+
exp.r_sphere = iadpython.Sphere(x[7], x[8], x[9], x[10], 0, x[11])
|
|
173
|
+
|
|
174
|
+
if exp.num_spheres > 0:
|
|
175
|
+
exp.t_sphere = iadpython.Sphere(x[12], x[13], x[14], x[15], 0, x[16])
|
|
176
|
+
|
|
177
|
+
# Read data
|
|
178
|
+
if column_letters_str == "":
|
|
179
|
+
# old style data format
|
|
180
|
+
exp.num_measures = x[17]
|
|
181
|
+
data = x[18:]
|
|
182
|
+
columns = int(exp.num_measures)
|
|
183
|
+
if data[0] > 1:
|
|
184
|
+
columns += 1
|
|
185
|
+
data_in_columns = data.reshape(-1, columns)
|
|
186
|
+
fill_in_data_fixed(exp, data_in_columns)
|
|
187
|
+
else:
|
|
188
|
+
# new style variable header format
|
|
189
|
+
exp.num_measures = 0
|
|
190
|
+
if "r" in column_letters_str:
|
|
191
|
+
exp.num_measures += 1
|
|
192
|
+
if "t" in column_letters_str:
|
|
193
|
+
exp.num_measures += 1
|
|
194
|
+
if "u" in column_letters_str:
|
|
195
|
+
exp.num_measures += 1
|
|
196
|
+
data = x[17:]
|
|
197
|
+
columns = len(column_letters_str)
|
|
198
|
+
data_in_columns = data.reshape(-1, columns)
|
|
199
|
+
fill_in_data_variable(exp, data_in_columns, column_letters_str)
|
|
200
|
+
|
|
201
|
+
return exp
|