miepython 3.3.0__py3-none-any.whl → 3.3.1__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.
- miepython/__init__.py +1 -1
- miepython/core.py +67 -0
- miepython/field.py +2 -1
- miepython/monte_carlo.py +128 -86
- miepython/rayleigh.py +179 -15
- {miepython-3.3.0.dist-info → miepython-3.3.1.dist-info}/METADATA +5 -6
- miepython-3.3.1.dist-info/RECORD +18 -0
- {miepython-3.3.0.dist-info → miepython-3.3.1.dist-info}/WHEEL +1 -1
- miepython-3.3.0.dist-info/RECORD +0 -18
- {miepython-3.3.0.dist-info → miepython-3.3.1.dist-info}/licenses/LICENSE.txt +0 -0
- {miepython-3.3.0.dist-info → miepython-3.3.1.dist-info}/top_level.txt +0 -0
miepython/__init__.py
CHANGED
miepython/core.py
CHANGED
|
@@ -69,6 +69,55 @@ def wiscombe_terms(x):
|
|
|
69
69
|
return int(x + 4.05 * x**0.33333 + 2.0)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def _check_m_x(m, x):
|
|
73
|
+
"""
|
|
74
|
+
Reject an index or size parameter that the kernels would silently misread.
|
|
75
|
+
|
|
76
|
+
The kernels treat any index whose real part fails ``m.real > 0`` as a perfect
|
|
77
|
+
conductor, so a NaN or negative real part gave the answer for a metal sphere
|
|
78
|
+
instead of an error. A NaN or infinite x failed differently on each backend.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
m: refractive index of the sphere, scalar or array
|
|
82
|
+
x: size parameter of the sphere, scalar or array
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValueError: if m or x is not finite, x is negative, or m.real is negative
|
|
86
|
+
"""
|
|
87
|
+
m = np.asarray(m)
|
|
88
|
+
x = np.asarray(x)
|
|
89
|
+
if not np.all(np.isfinite(m)):
|
|
90
|
+
raise ValueError(f"refractive index must be finite, got m={m}")
|
|
91
|
+
if np.any(m.real < 0):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"real part of the refractive index must not be negative, got m={m}; "
|
|
94
|
+
"use m.real == 0 for a perfect conductor"
|
|
95
|
+
)
|
|
96
|
+
if not np.all(np.isfinite(x)):
|
|
97
|
+
raise ValueError(f"size parameter must be finite, got x={x}")
|
|
98
|
+
if np.any(x < 0):
|
|
99
|
+
raise ValueError(f"size parameter must not be negative, got x={x}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _check_one_sphere(m, x):
|
|
103
|
+
"""
|
|
104
|
+
Reject arrays of m or x in functions that describe a single sphere.
|
|
105
|
+
|
|
106
|
+
The angular functions vectorize over mu, so an array of spheres has no
|
|
107
|
+
unambiguous shape to return. Without this check numpy raised "truth value of
|
|
108
|
+
an array is ambiguous" from deep inside.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
m: refractive index of the sphere
|
|
112
|
+
x: size parameter of the sphere
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: if m or x is not a scalar
|
|
116
|
+
"""
|
|
117
|
+
if np.ndim(m) > 0 or np.ndim(x) > 0:
|
|
118
|
+
raise ValueError("this function takes one sphere, so m and x must be scalars; loop over spheres for more")
|
|
119
|
+
|
|
120
|
+
|
|
72
121
|
def coefficients(m, x, n_pole=0, internal=False):
|
|
73
122
|
"""
|
|
74
123
|
Computes the Mie coefficients for a sphere.
|
|
@@ -103,6 +152,10 @@ def coefficients(m, x, n_pole=0, internal=False):
|
|
|
103
152
|
automatically corrected to its conjugate value to ensure a valid input.
|
|
104
153
|
- Every returned coefficient is a real term; the arrays carry no padding.
|
|
105
154
|
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: if m or x is not finite, x is negative, or the real part of
|
|
157
|
+
m is negative. A perfect conductor is m.real == 0, not m.real < 0.
|
|
158
|
+
|
|
106
159
|
Examples:
|
|
107
160
|
Compute coefficients for a single sphere:
|
|
108
161
|
|
|
@@ -123,6 +176,7 @@ def coefficients(m, x, n_pole=0, internal=False):
|
|
|
123
176
|
>>> print(f"{a[0, 0]:.6f}")
|
|
124
177
|
0.447946-0.386652j
|
|
125
178
|
"""
|
|
179
|
+
_check_m_x(m, x)
|
|
126
180
|
mlen = np.size(m) if np.ndim(m) > 0 else 0
|
|
127
181
|
xlen = np.size(x) if np.ndim(x) > 0 else 0
|
|
128
182
|
|
|
@@ -208,6 +262,10 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
|
|
|
208
262
|
- This implementation assumes spherical, homogeneous particles in a
|
|
209
263
|
non-absorbing medium.
|
|
210
264
|
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError: if m or x is not finite, x is negative, or the real part of
|
|
267
|
+
m is negative. A perfect conductor is m.real == 0, not m.real < 0.
|
|
268
|
+
|
|
211
269
|
Examples:
|
|
212
270
|
Compute efficiencies for one sphere:
|
|
213
271
|
|
|
@@ -228,6 +286,8 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
|
|
|
228
286
|
Use :func:`efficiencies` instead when the diameter and wavelength are
|
|
229
287
|
known rather than the size parameter.
|
|
230
288
|
"""
|
|
289
|
+
_check_m_x(m, x)
|
|
290
|
+
|
|
231
291
|
# ensure imaginary part of refractive index is negative
|
|
232
292
|
if np.isscalar(m):
|
|
233
293
|
m = np.conj(m) if np.imag(m) > 0 else m
|
|
@@ -361,7 +421,14 @@ def S1_S2(m, x, mu, norm="albedo", n_pole=0):
|
|
|
361
421
|
|
|
362
422
|
Returns:
|
|
363
423
|
S1, S2: the scattering amplitudes at each angle mu [sr**(-0.5)]
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
ValueError: if m or x is not finite, x is negative, or the real part of
|
|
427
|
+
m is negative. A perfect conductor is m.real == 0, not m.real < 0.
|
|
428
|
+
Also if m or x is an array, since this describes one sphere.
|
|
364
429
|
"""
|
|
430
|
+
_check_m_x(m, x)
|
|
431
|
+
_check_one_sphere(m, x)
|
|
365
432
|
if np.imag(m) > 0: # ensure imaginary part of refractive index is negative
|
|
366
433
|
m = np.conj(m)
|
|
367
434
|
|
miepython/field.py
CHANGED
|
@@ -82,7 +82,7 @@ import numpy as np
|
|
|
82
82
|
from scipy.special import factorial2, spherical_jn
|
|
83
83
|
from ._backend import D_calc, pi_tau
|
|
84
84
|
from .bessel import spherical_h1
|
|
85
|
-
from .core import S1_S2, coefficients, wiscombe_terms
|
|
85
|
+
from .core import S1_S2, _check_m_x, coefficients, wiscombe_terms
|
|
86
86
|
from .util import cartesian_to_spherical, spherical_vector_to_cartesian
|
|
87
87
|
|
|
88
88
|
__all__ = (
|
|
@@ -287,6 +287,7 @@ def _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole):
|
|
|
287
287
|
"""
|
|
288
288
|
x = np.pi * d_sphere * n_env / lambda0
|
|
289
289
|
m_rel = m_sphere / n_env
|
|
290
|
+
_check_m_x(m_rel, x) # before wiscombe_terms, which cannot take a NaN x
|
|
290
291
|
if n_pole == 0:
|
|
291
292
|
# Wiscombe's criterion truncates the scattered series, which converges
|
|
292
293
|
# faster than the field evaluated right at the surface. Now that an_bn
|
miepython/monte_carlo.py
CHANGED
|
@@ -1,37 +1,64 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Sample scattering angles from the Mie phase function.
|
|
3
|
-
|
|
4
|
-
Monte Carlo
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
2
|
+
Sample random scattering angles from the Mie phase function.
|
|
3
|
+
|
|
4
|
+
A Monte Carlo simulation of light transport needs, at every scattering event, a
|
|
5
|
+
random direction in which the photon leaves the sphere. For unpolarized light
|
|
6
|
+
only the polar angle theta depends on the sphere, and it must be drawn so that
|
|
7
|
+
its cosine mu follows the phase function. This module does that by inverse
|
|
8
|
+
transform sampling: build the cumulative distribution of mu once, then map each
|
|
9
|
+
uniform random number u to the cosine at which the distribution reaches u.
|
|
10
|
+
|
|
11
|
+
``cdf``
|
|
12
|
+
The cumulative distribution at evenly spaced cosines.
|
|
13
|
+
``mu_with_uniform_cdf``
|
|
14
|
+
The cosines at which the cumulative distribution reaches evenly spaced
|
|
15
|
+
probabilities, a lookup table that makes each draw cheap.
|
|
16
|
+
``generate_mie_costheta``
|
|
17
|
+
One random cosine drawn from that table.
|
|
18
|
+
|
|
19
|
+
``docs/06_random_deviates.ipynb`` works through the whole procedure.
|
|
20
|
+
|
|
21
|
+
This module is *not* imported with the package, because it imports
|
|
22
|
+
``miepython`` itself and doing so from ``miepython/__init__.py`` would be
|
|
23
|
+
circular. Import it by name, as the examples below do.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
Invert the distribution with ``np.interp`` to draw many cosines at once.
|
|
27
|
+
Their average is the asymmetry parameter g:
|
|
28
|
+
|
|
29
|
+
>>> import numpy as np
|
|
30
|
+
>>> import miepython as mie
|
|
31
|
+
>>> import miepython.monte_carlo as mc
|
|
32
|
+
>>> m, x = 1.33, 2.0 # a water droplet
|
|
33
|
+
>>> mu, cdf = mc.cdf(m, x, 1000)
|
|
34
|
+
>>> rng = np.random.default_rng(42)
|
|
35
|
+
>>> cosines = np.interp(rng.random(100_000), cdf, mu)
|
|
36
|
+
>>> print(f"{np.mean(cosines):.2f}")
|
|
37
|
+
0.67
|
|
38
|
+
>>> qext, qsca, qback, g = mie.efficiencies_mx(m, x)
|
|
39
|
+
>>> print(f"{g:.2f}")
|
|
40
|
+
0.67
|
|
41
|
+
|
|
42
|
+
Or build a lookup table once and draw one cosine at a time, as a photon
|
|
43
|
+
transport loop does:
|
|
44
|
+
|
|
45
|
+
>>> mu_table, _ = mc.mu_with_uniform_cdf(m, x, 1000)
|
|
46
|
+
>>> cosines = [mc.generate_mie_costheta(mu_table, rng=rng) for _ in range(100_000)]
|
|
47
|
+
>>> print(f"{np.mean(cosines):.2f}")
|
|
48
|
+
0.67
|
|
49
|
+
|
|
50
|
+
Building either table costs a full Mie calculation, while each draw costs only a
|
|
51
|
+
random number or two, so build it once and reuse it. Evenly spaced cosines
|
|
52
|
+
resolve the narrow forward peak of a large sphere poorly; ``cdf`` explains how
|
|
53
|
+
many are needed and warns when there are too few, while ``mu_with_uniform_cdf``
|
|
54
|
+
chooses enough by itself.
|
|
30
55
|
|
|
31
56
|
Passing ``rng`` keeps the draws reproducible without disturbing the process-wide
|
|
32
57
|
``numpy.random``; omitting it falls back to that global generator.
|
|
33
58
|
"""
|
|
34
59
|
|
|
60
|
+
import warnings
|
|
61
|
+
|
|
35
62
|
import numpy as np
|
|
36
63
|
import miepython as mie
|
|
37
64
|
|
|
@@ -41,87 +68,102 @@ __all__ = (
|
|
|
41
68
|
"generate_mie_costheta",
|
|
42
69
|
)
|
|
43
70
|
|
|
71
|
+
# Largest acceptable relative error in the integral of the phase function
|
|
72
|
+
CDF_TOLERANCE = 0.01
|
|
73
|
+
|
|
44
74
|
|
|
45
75
|
def cdf(m, x, num):
|
|
46
76
|
"""
|
|
47
|
-
|
|
77
|
+
Cumulative distribution of the unpolarized phase function in cos(theta).
|
|
48
78
|
|
|
49
|
-
The
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
The cosines mu are evenly spaced from -1 (backscattering) to 1 (forward
|
|
80
|
+
scattering), and cdf[i] is the probability that a scattered photon leaves
|
|
81
|
+
with a cosine no greater than mu[i]. It is found by integrating the phase
|
|
82
|
+
function with the trapezoidal rule, so cdf[0] is exactly 0 and the values
|
|
83
|
+
never decrease.
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
A large sphere scatters much of its light into a diffraction peak about
|
|
86
|
+
4/x radians wide around the forward direction. Near theta = 0, mu = cos(theta)
|
|
87
|
+
is close to 1 - theta**2/2, so that peak spans only about 8/x**2 in mu,
|
|
88
|
+
while the cosines are 2/(num - 1) apart. To put several cosines inside the
|
|
89
|
+
peak, num must therefore grow roughly as x**2; x = 30 needs about 5000.
|
|
90
|
+
|
|
91
|
+
The phase function integrates to exactly one, so the raw trapezoidal sum
|
|
92
|
+
shows how well num resolves it. If that sum is off by more than
|
|
93
|
+
``CDF_TOLERANCE``, a ``RuntimeWarning`` is issued; increase num until it
|
|
94
|
+
stops. The result is then divided by the sum so that cdf[-1] is exactly 1.
|
|
57
95
|
|
|
58
96
|
Args:
|
|
59
|
-
m:
|
|
60
|
-
x:
|
|
61
|
-
num:
|
|
97
|
+
m: complex index of refraction of the sphere
|
|
98
|
+
x: size parameter of the sphere
|
|
99
|
+
num: number of cosines, at least two
|
|
62
100
|
|
|
63
101
|
Returns:
|
|
64
|
-
mu:
|
|
65
|
-
cdf:
|
|
102
|
+
mu: evenly spaced cosines of the scattering angle, from -1 to 1
|
|
103
|
+
cdf: cumulative probability at each cosine, from 0 to 1
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: if num is less than two or the phase function is not finite
|
|
66
107
|
"""
|
|
108
|
+
if num < 2:
|
|
109
|
+
raise ValueError("num must be at least 2")
|
|
110
|
+
return _trapezoid_cdf(m, x, num, stacklevel=3)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _trapezoid_cdf(m, x, num, stacklevel):
|
|
114
|
+
"""Integrate the phase function at num cosines, warning at the caller's caller."""
|
|
67
115
|
mu = np.linspace(-1, 1, num)
|
|
68
|
-
intensity_per_mu = mie.i_unpolarized(m, x, mu, norm="4pi") /
|
|
69
|
-
cdf_values = np.
|
|
116
|
+
intensity_per_mu = mie.i_unpolarized(m, x, mu, norm="4pi") / 2
|
|
117
|
+
cdf_values = np.zeros(num)
|
|
118
|
+
cdf_values[1:] = np.cumsum((intensity_per_mu[1:] + intensity_per_mu[:-1]) * np.diff(mu) / 2)
|
|
119
|
+
total = cdf_values[-1]
|
|
120
|
+
if not np.isfinite(total):
|
|
121
|
+
raise ValueError(f"phase function is not finite for m={m}, x={x}")
|
|
122
|
+
if abs(total - 1) > CDF_TOLERANCE:
|
|
123
|
+
warnings.warn(
|
|
124
|
+
f"phase function under-resolved for x={x}: integral over {num} angles is {total:.4g}, "
|
|
125
|
+
"not 1, so sampled angles will be inaccurate; use more angles",
|
|
126
|
+
RuntimeWarning,
|
|
127
|
+
stacklevel=stacklevel,
|
|
128
|
+
)
|
|
129
|
+
cdf_values /= total
|
|
70
130
|
return mu, cdf_values
|
|
71
131
|
|
|
72
132
|
|
|
73
133
|
def mu_with_uniform_cdf(m, x, num):
|
|
74
134
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
The CDF covers scattered (exit) angles ranging from 180 to 0 degrees.
|
|
78
|
-
(The cosines are uniformly distributed over -1 to 1.) These angles mu
|
|
79
|
-
correspond to uniform spacing of the cumulative distribution function
|
|
80
|
-
for unpolarized Mie scattering where cdf[i] = i/(num-1).
|
|
135
|
+
Inverse-transform table for sampling the unpolarized phase function.
|
|
81
136
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
137
|
+
The table holds num cosines mu running from -1 (backscattering) to 1
|
|
138
|
+
(forward scattering), spaced so that a scattered photon is equally likely
|
|
139
|
+
to fall between any two neighbours: cdf[i] is exactly i/(num - 1). The
|
|
140
|
+
cosines are therefore bunched where scattering is strong and sparse where
|
|
141
|
+
it is weak. ``generate_mie_costheta`` draws random cosines from this table.
|
|
85
142
|
|
|
86
|
-
|
|
87
|
-
|
|
143
|
+
The table is found by calculating ``cdf`` at max(2000, 5*x**2) evenly
|
|
144
|
+
spaced cosines, enough to resolve the forward peak whatever num is (see
|
|
145
|
+
``cdf``), and interpolating to find where it reaches each step. Because
|
|
146
|
+
that count grows as x**2, building the table takes seconds at x = 100 and
|
|
147
|
+
over a minute at x = 300.
|
|
88
148
|
|
|
89
149
|
Args:
|
|
90
|
-
m:
|
|
91
|
-
x:
|
|
92
|
-
num:
|
|
150
|
+
m: complex index of refraction of the sphere
|
|
151
|
+
x: size parameter of the sphere
|
|
152
|
+
num: number of entries in the table, at least two
|
|
93
153
|
|
|
94
154
|
Returns:
|
|
95
|
-
mu:
|
|
96
|
-
cdf:
|
|
97
|
-
"""
|
|
98
|
-
big_num = 2000 # large to work with x up to 10
|
|
99
|
-
big_mu, big_cdf = cdf(m, x, big_num)
|
|
100
|
-
mu = np.empty(num)
|
|
101
|
-
cdf_values = np.empty(num)
|
|
102
|
-
|
|
103
|
-
mu[0] = -1 # cos[180 degrees] is -1
|
|
104
|
-
cdf_values[0] = 0 # initial cdf is zero
|
|
105
|
-
|
|
106
|
-
big_k = 0 # index into big_cdf
|
|
107
|
-
for k in range(1, num - 1):
|
|
155
|
+
mu: unevenly spaced cosines of the scattering angle, from -1 to 1
|
|
156
|
+
cdf: evenly spaced cumulative probabilities, from 0 to 1
|
|
108
157
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
cdf_values[k] = target
|
|
119
|
-
|
|
120
|
-
# print(' mu[',k,']=% .5f'%mu[k],' cdf[',k,']=% .5f'%cdf[k],
|
|
121
|
-
# 'cdf=',big_cdf[big_k], fraction)
|
|
122
|
-
|
|
123
|
-
mu[num - 1] = 1 # cos[0 degrees] is 1
|
|
124
|
-
cdf_values[num - 1] = 1 # last cdf is one
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: if num is less than two or the phase function is not finite
|
|
160
|
+
"""
|
|
161
|
+
if num < 2:
|
|
162
|
+
raise ValueError("num must be at least 2")
|
|
163
|
+
big_num = max(2000, int(5 * x**2)) # resolve the forward peak, see cdf
|
|
164
|
+
big_mu, big_cdf = _trapezoid_cdf(m, x, big_num, stacklevel=3)
|
|
165
|
+
cdf_values = np.arange(num) / (num - 1)
|
|
166
|
+
mu = np.interp(cdf_values, big_cdf, big_mu)
|
|
125
167
|
|
|
126
168
|
return [mu, cdf_values]
|
|
127
169
|
|
miepython/rayleigh.py
CHANGED
|
@@ -1,7 +1,125 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Rayleigh approximation for scattering by spheres much smaller than the wavelength.
|
|
3
|
+
|
|
4
|
+
When a sphere is small both outside (x << 1) and inside (abs(m) * x << 1), the
|
|
5
|
+
incident field is nearly uniform across it, and it radiates as a single
|
|
6
|
+
oscillating electric dipole. Every scattering quantity then follows in closed
|
|
7
|
+
form from the ratio (m**2 - 1) / (m**2 + 2), with no series of multipoles to
|
|
8
|
+
sum. This module evaluates those expressions, following Bohren and Huffman,
|
|
9
|
+
equations 5.4 and 5.7 to 5.9. A perfect conductor also radiates a magnetic
|
|
10
|
+
dipole, and has its own expressions, described below.
|
|
11
|
+
|
|
12
|
+
The functions mirror those of the same names in ``miepython``, with the same
|
|
13
|
+
arguments and the same ``norm`` choices, so one can stand in for the other:
|
|
14
|
+
|
|
15
|
+
``efficiencies_mx(m, x)``, ``efficiencies(m, d, lambda0, n_env)``
|
|
16
|
+
qext, qsca, qback and g.
|
|
17
|
+
``S1_S2(m, x, mu, norm)``
|
|
18
|
+
The scattering amplitudes at each mu = cos(theta).
|
|
19
|
+
``i_par``, ``i_per``, ``i_unpolarized``, ``intensities``
|
|
20
|
+
Scattered intensities at each mu.
|
|
21
|
+
``phase_matrix(m, x, mu, norm)``
|
|
22
|
+
The 4x4 Mueller matrix at each mu.
|
|
23
|
+
|
|
24
|
+
Nothing here takes ``n_pole``, since there is only the dipole.
|
|
25
|
+
|
|
26
|
+
Order of approximation:
|
|
27
|
+
Strictly, Rayleigh scattering is only the leading term in x: an electric
|
|
28
|
+
dipole whose strength is set by the static polarizability. Not every
|
|
29
|
+
expression here stops at that term, so this is better described as a
|
|
30
|
+
small-particle approximation.
|
|
31
|
+
|
|
32
|
+
- qsca and qback are the leading terms, 8/3 x**4 abs(ratio)**2 and
|
|
33
|
+
4 x**4 abs(ratio)**2.
|
|
34
|
+
- qext carries Bohren and Huffman's next-order correction. Its absorption
|
|
35
|
+
term includes a relative x**2 correction, the order at which the
|
|
36
|
+
magnetic dipole and the electric quadrupole first contribute (the
|
|
37
|
+
2 m**2 + 3 in its denominator is the quadrupole's), and scattering is
|
|
38
|
+
added as 8/3 x**4 Re(ratio**2).
|
|
39
|
+
- ``S1_S2`` gives the electric dipole its own relative x**2 correction, the
|
|
40
|
+
x**5 term, but leaves out the magnetic dipole and quadrupole. The angular
|
|
41
|
+
pattern is therefore exactly that of a dipole, and g is 0.
|
|
42
|
+
- A perfect conductor gets only the leading terms of its electric and
|
|
43
|
+
magnetic dipoles.
|
|
44
|
+
|
|
45
|
+
The corrections are not applied evenly, so a dielectric's qext is more
|
|
46
|
+
accurate than its qsca. ``miepython`` itself goes further for x < 0.1,
|
|
47
|
+
using Wiscombe's expansions, which keep the electric dipole, the magnetic
|
|
48
|
+
dipole and the electric quadrupole, each with its own corrections.
|
|
49
|
+
|
|
50
|
+
Accuracy:
|
|
51
|
+
Relative error in qext against the full Mie solution:
|
|
52
|
+
|
|
53
|
+
================= ========= ======== ========
|
|
54
|
+
m x = 0.01 x = 0.1 x = 0.5
|
|
55
|
+
================= ========= ======== ========
|
|
56
|
+
1.5 -7e-6 -7e-4 -1e-2
|
|
57
|
+
1.5 - 0.1j 8e-10 6e-6 4e-3
|
|
58
|
+
0.05 - 1.43j 2e-6 2e-2 14
|
|
59
|
+
-1j (conductor) -2e-5 -2e-3 -4e-2
|
|
60
|
+
================= ========= ======== ========
|
|
61
|
+
|
|
62
|
+
The last row is a silver-like sphere near its plasmon resonance, where
|
|
63
|
+
m**2 is close to -2 and the denominator of the ratio nearly vanishes. There
|
|
64
|
+
the approximation fails at much smaller x than for an ordinary dielectric.
|
|
65
|
+
|
|
66
|
+
For a dielectric the asymmetry parameter g is 0, because dipole scattering
|
|
67
|
+
is the same forwards and backwards. The Mie value is 0.002 at x = 0.1 and
|
|
68
|
+
0.05 at x = 0.5 for m = 1.5.
|
|
69
|
+
|
|
70
|
+
Conventions:
|
|
71
|
+
The index is m = n - ik, as in the rest of the package, and an index with a
|
|
72
|
+
positive imaginary part is conjugated. The inputs that ``miepython``
|
|
73
|
+
rejects, a NaN or infinite m or x, a negative x or a negative real part of
|
|
74
|
+
m, raise the same ``ValueError`` here. The efficiencies accept an array of
|
|
75
|
+
m or x and return arrays, g included. The angular functions describe one
|
|
76
|
+
sphere and vectorize over mu only.
|
|
77
|
+
|
|
78
|
+
Perfect conductors:
|
|
79
|
+
As in the rest of the package, m.real == 0 means a perfect conductor,
|
|
80
|
+
whatever the imaginary part, and m = 0 is one too. No field enters such a
|
|
81
|
+
sphere, so the dielectric expressions, which describe the field inside,
|
|
82
|
+
do not apply. Instead the surface currents radiate an electric dipole and
|
|
83
|
+
a magnetic dipole half as strong and opposite in sign, a1 = 2i x**3/3 and
|
|
84
|
+
b1 = -a1/2. The sphere absorbs nothing, qext = qsca = 10/3 x**4, and
|
|
85
|
+
qback = 9 x**4. The two dipoles add backwards and partly cancel forwards,
|
|
86
|
+
so g = -2/5 and the backscattered intensity is nine times the forward one.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
A small absorbing sphere, compared with the full Mie solution:
|
|
90
|
+
|
|
91
|
+
>>> import numpy as np
|
|
92
|
+
>>> import miepython as mie
|
|
93
|
+
>>> from miepython import rayleigh
|
|
94
|
+
>>> qext, qsca, qback, g = rayleigh.efficiencies_mx(1.5 - 0.1j, 0.1)
|
|
95
|
+
>>> print(f"{qext:.5f} {qsca:.6f} {g:.4f}")
|
|
96
|
+
0.02006 0.000024 0.0000
|
|
97
|
+
>>> qext, qsca, qback, g = mie.efficiencies_mx(1.5 - 0.1j, 0.1)
|
|
98
|
+
>>> print(f"{qext:.5f} {qsca:.6f} {g:.4f}")
|
|
99
|
+
0.02006 0.000024 0.0020
|
|
100
|
+
|
|
101
|
+
The dipole pattern scatters equally forwards and backwards, and half as
|
|
102
|
+
much at 90 degrees:
|
|
103
|
+
|
|
104
|
+
>>> mu = np.array([-1.0, 0.0, 1.0])
|
|
105
|
+
>>> print(np.round(rayleigh.i_unpolarized(1.5, 0.1, mu), 4))
|
|
106
|
+
[0.1195 0.0597 0.1195]
|
|
107
|
+
>>> print(np.round(mie.i_unpolarized(1.5, 0.1, mu), 4))
|
|
108
|
+
[0.1188 0.0597 0.1199]
|
|
109
|
+
|
|
110
|
+
A perfect conductor scatters mostly backwards:
|
|
111
|
+
|
|
112
|
+
>>> qext, qsca, qback, g = rayleigh.efficiencies_mx(-1j, 0.1)
|
|
113
|
+
>>> print(f"{qsca:.4e} {qback:.4e} {g:.4f}")
|
|
114
|
+
3.3333e-04 9.0000e-04 -0.4000
|
|
115
|
+
>>> qext, qsca, qback, g = mie.efficiencies_mx(-1j, 0.1)
|
|
116
|
+
>>> print(f"{qsca:.4e} {qback:.4e} {g:.4f}")
|
|
117
|
+
3.3413e-04 8.9834e-04 -0.3973
|
|
118
|
+
"""
|
|
2
119
|
|
|
3
120
|
import numpy as np
|
|
4
121
|
|
|
122
|
+
from .core import _check_m_x, _check_one_sphere
|
|
5
123
|
from .core import normalization_factor as core_normalization_factor
|
|
6
124
|
|
|
7
125
|
__all__ = (
|
|
@@ -20,7 +138,9 @@ def efficiencies_mx(m, x):
|
|
|
20
138
|
"""
|
|
21
139
|
Calculate the efficiencies for a small sphere.
|
|
22
140
|
|
|
23
|
-
Based on equations 5.7 - 5.9 in Bohren and Huffman
|
|
141
|
+
Based on equations 5.7 - 5.9 in Bohren and Huffman. A perfect conductor,
|
|
142
|
+
m.real == 0, uses the electric and magnetic dipoles instead; the module
|
|
143
|
+
docstring gives the details.
|
|
24
144
|
|
|
25
145
|
Args:
|
|
26
146
|
m: the complex index of refraction of the sphere
|
|
@@ -30,14 +150,43 @@ def efficiencies_mx(m, x):
|
|
|
30
150
|
qext: the total extinction efficiency
|
|
31
151
|
qsca: the scattering efficiency
|
|
32
152
|
qback: the backscatter efficiency
|
|
33
|
-
g: the average cosine of the scattering phase function
|
|
153
|
+
g: the average cosine of the scattering phase function, 0 for a dielectric, -0.4 for a conductor
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: if m or x is not finite, x is negative, or the real part of
|
|
157
|
+
m is negative, exactly as for the full Mie functions
|
|
34
158
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
159
|
+
_check_m_x(m, x)
|
|
160
|
+
|
|
161
|
+
# ensure imaginary part of refractive index is negative, as core.py does
|
|
162
|
+
if np.isscalar(m):
|
|
163
|
+
m = np.conj(m) if np.imag(m) > 0 else m
|
|
164
|
+
else:
|
|
165
|
+
m = np.where(np.imag(m) > 0, np.conj(m), m)
|
|
166
|
+
|
|
167
|
+
# the dielectric expressions are evaluated for every entry and then replaced
|
|
168
|
+
# where m is a conductor; a few conductor indices, m = -1j*sqrt(2) and
|
|
169
|
+
# m = -1j*sqrt(1.5), make their denominators vanish, so silence the warnings
|
|
170
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
171
|
+
ratio = (m**2 - 1) / (m**2 + 2)
|
|
172
|
+
qsca = 8 / 3 * x**4 * abs(ratio) ** 2
|
|
173
|
+
# Bohren & Huffman write m = n + ik, so with m = n - ik the absorption term
|
|
174
|
+
# changes sign. The x**4 term is Re(ratio**2), not |ratio|**2: the two only
|
|
175
|
+
# agree for a non-absorbing sphere, and the difference matters near resonance.
|
|
176
|
+
qabs = -4 * x * (ratio * (1 + x**2 / 15 * ratio * (m**4 + 27 * m**2 + 38) / (2 * m**2 + 3))).imag
|
|
177
|
+
qext = qabs + 8 / 3 * x**4 * (ratio**2).real
|
|
178
|
+
qback = 4 * x**4 * abs(ratio) ** 2
|
|
179
|
+
|
|
180
|
+
# A perfect conductor (m.real == 0) radiates a magnetic dipole half as strong
|
|
181
|
+
# as, and opposite to, its electric one: a1 = 2i x**3/3 and b1 = -a1/2. It
|
|
182
|
+
# absorbs nothing, scatters 6/x**2 (|a1|**2 + |b1|**2) = 10/3 x**4, and the
|
|
183
|
+
# interference of the two dipoles throws light backwards, so g = -2/5.
|
|
184
|
+
conductor = np.real(m) == 0
|
|
185
|
+
qsca = np.where(conductor, 10 / 3 * x**4, qsca)[()]
|
|
186
|
+
qext = np.where(conductor, 10 / 3 * x**4, qext)[()]
|
|
187
|
+
qback = np.where(conductor, 9 * x**4, qback)[()]
|
|
188
|
+
# a dielectric's electric dipole alone scatters symmetrically fore and aft
|
|
189
|
+
g = np.where(conductor, -0.4, 0 * qsca)[()]
|
|
41
190
|
return qext, qsca, qback, g
|
|
42
191
|
|
|
43
192
|
|
|
@@ -84,7 +233,8 @@ def S1_S2(m, x, mu, norm="albedo"):
|
|
|
84
233
|
"""
|
|
85
234
|
Calculate the scattering amplitude functions for small spheres.
|
|
86
235
|
|
|
87
|
-
Based on equation 5.4 in Bohren and Huffman
|
|
236
|
+
Based on equation 5.4 in Bohren and Huffman. A perfect conductor,
|
|
237
|
+
m.real == 0, adds the magnetic dipole b1 = -a1/2 to its electric one.
|
|
88
238
|
|
|
89
239
|
The amplitude functions are normalized so that when integrated
|
|
90
240
|
over all 4*pi solid angles, the integral will be qext*pi*x**2.
|
|
@@ -99,15 +249,29 @@ def S1_S2(m, x, mu, norm="albedo"):
|
|
|
99
249
|
|
|
100
250
|
Returns:
|
|
101
251
|
S1, S2: the scattering amplitudes at each angle mu [sr**(-0.5)]
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: if m or x is not finite, x is negative, or the real part of
|
|
255
|
+
m is negative, exactly as for the full Mie functions, or if m or x
|
|
256
|
+
is an array, since this describes one sphere
|
|
102
257
|
"""
|
|
258
|
+
_check_m_x(m, x)
|
|
259
|
+
_check_one_sphere(m, x)
|
|
103
260
|
if np.imag(m) > 0: # ensure imaginary part of refractive index is negative
|
|
104
261
|
m = np.conj(m)
|
|
105
262
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
263
|
+
if np.real(m) == 0:
|
|
264
|
+
# perfect conductor: an electric dipole and a magnetic one, b1 = -a1/2
|
|
265
|
+
a1 = 2j * x**3 / 3
|
|
266
|
+
b1 = -a1 / 2
|
|
267
|
+
else:
|
|
268
|
+
a1 = (2 * x**3) / 3 * (m**2 - 1) / (m**2 + 2) * 1j
|
|
269
|
+
a1 += (2 * x**5) / 5 * (m**2 - 2) * (m**2 - 1) / (m**2 + 2) ** 2 * 1j
|
|
270
|
+
b1 = 0
|
|
271
|
+
|
|
272
|
+
# the n = 1 terms of the Mie series, where pi_1 = 1 and tau_1 = mu
|
|
273
|
+
S1 = (3 / 2) * (a1 * np.ones_like(mu) + b1 * mu)
|
|
274
|
+
S2 = (3 / 2) * (a1 * mu + b1 * np.ones_like(mu))
|
|
111
275
|
|
|
112
276
|
normalization = normalization_factor(m, x, norm)
|
|
113
277
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: miepython
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: Mie scattering of a plane wave by a sphere
|
|
5
5
|
Author-email: Scott Prahl <scott.prahl@oit.edu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,12 +14,12 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
-
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
23
|
Description-Content-Type: text/x-rst
|
|
24
24
|
License-File: LICENSE.txt
|
|
25
25
|
Requires-Dist: numpy
|
|
@@ -47,7 +47,6 @@ Requires-Dist: build; extra == "dev"
|
|
|
47
47
|
Requires-Dist: check-manifest; extra == "dev"
|
|
48
48
|
Requires-Dist: libarchive-c; extra == "dev"
|
|
49
49
|
Requires-Dist: pyyaml; extra == "dev"
|
|
50
|
-
Requires-Dist: setuptools; extra == "dev"
|
|
51
50
|
Requires-Dist: jupyterlab; extra == "dev"
|
|
52
51
|
Requires-Dist: nbconvert; extra == "dev"
|
|
53
52
|
Requires-Dist: nbformat; extra == "dev"
|
|
@@ -211,7 +210,7 @@ Citation
|
|
|
211
210
|
|
|
212
211
|
If you use ``miepython`` in academic, instructional, or applied technical work, please cite:
|
|
213
212
|
|
|
214
|
-
Prahl, S. (2026). *miepython: Pure python calculation of Mie scattering* (Version 3.
|
|
213
|
+
Prahl, S. (2026). *miepython: Pure python calculation of Mie scattering* (Version 3.3.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.7949263
|
|
215
214
|
|
|
216
215
|
BibTeX:
|
|
217
216
|
|
|
@@ -223,7 +222,7 @@ BibTeX:
|
|
|
223
222
|
url = {https://github.com/scottprahl/miepython},
|
|
224
223
|
doi = {10.5281/zenodo.7949263},
|
|
225
224
|
year = {2026},
|
|
226
|
-
version = {3.
|
|
225
|
+
version = {3.3.0},
|
|
227
226
|
publisher = {Zenodo}
|
|
228
227
|
}
|
|
229
228
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
miepython/__init__.py,sha256=oWYwelBWxyV7Rc9DQSVKvy-Mx4Ck-k2PjfyShl-5pW0,5888
|
|
2
|
+
miepython/_backend.py,sha256=l_faAa6zanygRZKA9vMVcfcutHPFFkpqZzHqwjU8QcE,1619
|
|
3
|
+
miepython/bessel.py,sha256=U094wY_5RFqrVjXZgMbacmt_yqpP8GBtRZ24-MNzBsU,3647
|
|
4
|
+
miepython/core.py,sha256=zv0hkVBBpcr9GA2xwNAxWHancnh7o5X1mp2xZUoz2Js,24974
|
|
5
|
+
miepython/field.py,sha256=zvTf2g0qca0WRfJ9r2SL5YDGObfTYlOpcXUY9BoOQjA,26583
|
|
6
|
+
miepython/mie_jit.py,sha256=kYy9UCfuEgPSL0XY7P8Qx4IqBr9BbffusF6oeZdTnvQ,21685
|
|
7
|
+
miepython/mie_nojit.py,sha256=EGWqpvdnWOOV72x2wPy8TOSYXLguKXKQn-LzRXYpkrs,20315
|
|
8
|
+
miepython/monte_carlo.py,sha256=1pRELa63NcZOkhSe2AviObPztPaKe7JRqMBQn3rp9Ls,8422
|
|
9
|
+
miepython/rayleigh.py,sha256=6PMTNRs_0u3n9-7bBgJ9-QTB-pmGoPs29iOxj3Eub78,16939
|
|
10
|
+
miepython/util.py,sha256=3jxGd0jmPFYolllQXISLDmz0jE4C1AyFD1OyD65dUGo,3648
|
|
11
|
+
miepython/vsh.py,sha256=bPmEBCxbjCcNgisw3qQR-Zso46-aGPrnv9VBu7jhyTM,18114
|
|
12
|
+
miepython/data/Johnson.txt,sha256=PcdEhJ3ZKUnt--86ukEvUt-w-814uOyWXtE-G3qWTNE,1337
|
|
13
|
+
miepython/data/segelstein81_index.txt,sha256=aJLqcPIkw-0WDY37GRcTefLV0WGhyMt6IbOKY8JxGww,36719
|
|
14
|
+
miepython-3.3.1.dist-info/licenses/LICENSE.txt,sha256=g9-BWWRLvMDMQ1Zflxliku7Sleom1_SbXHQHOLGgqqk,1055
|
|
15
|
+
miepython-3.3.1.dist-info/METADATA,sha256=AlX1gk-tBQ2Z6km4sptmW2cGUx51DDAw4nEE0Gz30YE,7789
|
|
16
|
+
miepython-3.3.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
17
|
+
miepython-3.3.1.dist-info/top_level.txt,sha256=bfkk6YwrKm_aIzInR5y03YbywpwCzUjJEHaFkEiSKxI,10
|
|
18
|
+
miepython-3.3.1.dist-info/RECORD,,
|
miepython-3.3.0.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
miepython/__init__.py,sha256=DCQbT9uj3NC33GNii2P242pbCKFffe4tg7_qszNKpKE,5888
|
|
2
|
-
miepython/_backend.py,sha256=l_faAa6zanygRZKA9vMVcfcutHPFFkpqZzHqwjU8QcE,1619
|
|
3
|
-
miepython/bessel.py,sha256=U094wY_5RFqrVjXZgMbacmt_yqpP8GBtRZ24-MNzBsU,3647
|
|
4
|
-
miepython/core.py,sha256=SxppxzCwor0qFNMTPReBM6SZTDhuhuWyrnFEdm8fiQE,22519
|
|
5
|
-
miepython/field.py,sha256=3CFj0Tg764fT-BABSGjmhOGiBiUUsqDuVheXToGb7Wg,26494
|
|
6
|
-
miepython/mie_jit.py,sha256=kYy9UCfuEgPSL0XY7P8Qx4IqBr9BbffusF6oeZdTnvQ,21685
|
|
7
|
-
miepython/mie_nojit.py,sha256=EGWqpvdnWOOV72x2wPy8TOSYXLguKXKQn-LzRXYpkrs,20315
|
|
8
|
-
miepython/monte_carlo.py,sha256=ZEccQCkqlbqFOXmVmVzgWx41nBJ-Yuj45-cxYDLZjDY,6015
|
|
9
|
-
miepython/rayleigh.py,sha256=cWSmP-N5QXF2u7WPwew_QiI4gyJx4FAZNxSiKFvpCMo,8875
|
|
10
|
-
miepython/util.py,sha256=3jxGd0jmPFYolllQXISLDmz0jE4C1AyFD1OyD65dUGo,3648
|
|
11
|
-
miepython/vsh.py,sha256=bPmEBCxbjCcNgisw3qQR-Zso46-aGPrnv9VBu7jhyTM,18114
|
|
12
|
-
miepython/data/Johnson.txt,sha256=PcdEhJ3ZKUnt--86ukEvUt-w-814uOyWXtE-G3qWTNE,1337
|
|
13
|
-
miepython/data/segelstein81_index.txt,sha256=aJLqcPIkw-0WDY37GRcTefLV0WGhyMt6IbOKY8JxGww,36719
|
|
14
|
-
miepython-3.3.0.dist-info/licenses/LICENSE.txt,sha256=g9-BWWRLvMDMQ1Zflxliku7Sleom1_SbXHQHOLGgqqk,1055
|
|
15
|
-
miepython-3.3.0.dist-info/METADATA,sha256=e6yTtC4kocqZOZOCC7xN6XD3Ao00S2CKphQORmvesKs,7831
|
|
16
|
-
miepython-3.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
17
|
-
miepython-3.3.0.dist-info/top_level.txt,sha256=bfkk6YwrKm_aIzInR5y03YbywpwCzUjJEHaFkEiSKxI,10
|
|
18
|
-
miepython-3.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|