miepython 3.2.0__py3-none-any.whl → 3.3.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.
- miepython/__init__.py +33 -2
- miepython/core.py +105 -40
- miepython/field.py +238 -293
- miepython/mie_jit.py +202 -97
- miepython/mie_nojit.py +166 -86
- miepython/monte_carlo.py +57 -8
- miepython/rayleigh.py +7 -33
- miepython/util.py +25 -5
- miepython/vsh.py +16 -7
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/METADATA +6 -5
- miepython-3.3.0.dist-info/RECORD +18 -0
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/WHEEL +1 -1
- miepython-3.2.0.dist-info/RECORD +0 -18
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/top_level.txt +0 -0
miepython/mie_nojit.py
CHANGED
|
@@ -111,9 +111,73 @@ def _D_upwards(z, N, D):
|
|
|
111
111
|
D[n] = 1 / (n / z - D[n - 1]) - n / z
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
def _psi_downwards_py(z, nstop):
|
|
115
|
+
"""
|
|
116
|
+
Compute the Riccati-Bessel functions psi_0(z)..psi_nstop(z).
|
|
117
|
+
|
|
118
|
+
The three-term upwards recurrence
|
|
119
|
+
``psi_{n+1} = (2n+1)/z psi_n - psi_{n-1}`` is contaminated by the growing
|
|
120
|
+
chi_n solution once n exceeds ``|z|``, which is the normal case for psi_n(mx)
|
|
121
|
+
whenever the relative index is below one. Miller's algorithm is used
|
|
122
|
+
instead: seed the recurrence well above ``|z|`` where psi_n is the decaying
|
|
123
|
+
solution, run it downwards, then fix the arbitrary scale against whichever
|
|
124
|
+
of psi_0 or psi_1 is larger. sin(z) and cos(z) cannot both be small, so one
|
|
125
|
+
of those two seeds is always well conditioned.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
z: function argument (the refractive index times the size parameter)
|
|
129
|
+
nstop: highest order needed
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Array of psi_k(z) for k=0 to nstop
|
|
133
|
+
"""
|
|
134
|
+
abs_z = abs(z)
|
|
135
|
+
n_start = int(max(float(nstop), abs_z)) + 25 + int(1.5 * np.sqrt(abs_z))
|
|
136
|
+
|
|
137
|
+
psi = np.zeros(n_start + 2, dtype=np.complex128)
|
|
138
|
+
psi[n_start] = 1e-50
|
|
139
|
+
for n in range(n_start, 0, -1):
|
|
140
|
+
psi[n - 1] = (2 * n + 1) / z * psi[n] - psi[n + 1]
|
|
141
|
+
if abs(psi[n - 1]) > 1e200: # keep the growing tail in range
|
|
142
|
+
psi[n - 1 :] /= 1e200
|
|
143
|
+
|
|
144
|
+
sin_z = np.sin(z)
|
|
145
|
+
psi_0 = sin_z
|
|
146
|
+
psi_1 = sin_z / z - np.cos(z)
|
|
147
|
+
if abs(psi_1) > abs(psi_0):
|
|
148
|
+
psi *= psi_1 / psi[1]
|
|
149
|
+
else:
|
|
150
|
+
psi *= psi_0 / psi[0]
|
|
151
|
+
|
|
152
|
+
return psi[: nstop + 1]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _D_calc_down_py(z, N):
|
|
156
|
+
"""
|
|
157
|
+
Compute D_1(z)..D_N(z) using only the downwards recurrence.
|
|
158
|
+
|
|
159
|
+
``_D_calc_py`` picks between the upwards and downwards recurrences with
|
|
160
|
+
Wiscombe's criterion, which considers the refractive index but not the number
|
|
161
|
+
of terms. The upwards recurrence loses accuracy once N exceeds ``|z|`` (about
|
|
162
|
+
4 digits for a small sphere), and the resulting noise differs between the
|
|
163
|
+
numba and pure-python backends. The internal-field coefficients are not on
|
|
164
|
+
the hot path, so they always take the stable route.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
z: function argument (already multiplied by the refractive index)
|
|
168
|
+
N: highest order needed
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Array of logarithmic derivatives D_k(z) for k=1 to N
|
|
172
|
+
"""
|
|
173
|
+
D = np.zeros(N + 1, dtype=np.complex128)
|
|
174
|
+
_D_downwards(z, N, D)
|
|
175
|
+
return D[1:]
|
|
176
|
+
|
|
177
|
+
|
|
114
178
|
def _D_calc_py(m, x, N):
|
|
115
179
|
"""
|
|
116
|
-
Compute the logarithmic derivative of ψ_n(z) using the
|
|
180
|
+
Compute the logarithmic derivative of ψ_n(z) using the downwards recurrence.
|
|
117
181
|
|
|
118
182
|
D_n(z) = d[log ψ_n(z)] = ψ_n'(z)/ψ_n(z)
|
|
119
183
|
|
|
@@ -122,6 +186,14 @@ def _D_calc_py(m, x, N):
|
|
|
122
186
|
|
|
123
187
|
The zero-based array, D[:], is shifted so that D[0] = D₁(z) = ψ₁'(z)/ψ₁(z)
|
|
124
188
|
|
|
189
|
+
Wiscombe's criterion used to pick between the upwards and downwards
|
|
190
|
+
recurrences from the refractive index alone. The upwards recurrence is
|
|
191
|
+
contaminated once N passes ``|mx|``, which is the case for every small sphere,
|
|
192
|
+
and it left the Mie coefficients wrong by as much as 1% near m=1. The
|
|
193
|
+
downwards recurrence is stable everywhere, and the criterion already chose it
|
|
194
|
+
for most large spheres, so always taking it costs little. ``_D_upwards`` is
|
|
195
|
+
kept for comparison and testing.
|
|
196
|
+
|
|
125
197
|
Args:
|
|
126
198
|
m: the np.complex128 index of refraction of the sphere
|
|
127
199
|
x: the size parameter of the sphere
|
|
@@ -130,16 +202,7 @@ def _D_calc_py(m, x, N):
|
|
|
130
202
|
Returns:
|
|
131
203
|
Array of logarithmic derivatives D_k(z) for k=1 to N-1.
|
|
132
204
|
"""
|
|
133
|
-
|
|
134
|
-
kappa = np.abs(m.imag)
|
|
135
|
-
D = np.zeros(N + 1, dtype=np.complex128)
|
|
136
|
-
mx = np.complex128(m * x) # ensure np.complex128
|
|
137
|
-
|
|
138
|
-
if n < 1 or n > 10 or kappa > 10 or x * kappa >= 3.9 - 10.8 * n + 13.78 * n**2:
|
|
139
|
-
_D_downwards(mx, N, D)
|
|
140
|
-
else:
|
|
141
|
-
_D_upwards(mx, N, D)
|
|
142
|
-
return D[1:]
|
|
205
|
+
return _D_calc_down_py(np.complex128(m * x), N)
|
|
143
206
|
|
|
144
207
|
|
|
145
208
|
def _an_bn_py(m, x, n_pole=0):
|
|
@@ -150,8 +213,8 @@ def _an_bn_py(m, x, n_pole=0):
|
|
|
150
213
|
formula. The length of the arrays is chosen so that the error when the series
|
|
151
214
|
is summed is around 1e-6.
|
|
152
215
|
|
|
153
|
-
If n_pole>0, then the
|
|
154
|
-
trying to isolate the behavior of a particular multipole.
|
|
216
|
+
If n_pole>0, then the arrays hold exactly n_pole terms, orders 1 to n_pole.
|
|
217
|
+
This is useful when trying to isolate the behavior of a particular multipole.
|
|
155
218
|
|
|
156
219
|
To support resonance calculations, one can specify the number of terms
|
|
157
220
|
to be calculated. In general, using too few or too many terms increases the
|
|
@@ -164,58 +227,64 @@ def _an_bn_py(m, x, n_pole=0):
|
|
|
164
227
|
n_pole: the number of An and Bn terms (0 does autosizing)
|
|
165
228
|
|
|
166
229
|
Returns:
|
|
167
|
-
a, b: arrays of Mie coefficents An and Bn
|
|
230
|
+
a, b: arrays of Mie coefficents An and Bn, one entry per order,
|
|
231
|
+
with no padding. ``_cn_dn`` returns the same number of terms.
|
|
168
232
|
"""
|
|
169
233
|
if m.imag > 0: # ensure imaginary part of refractive index is negative
|
|
170
234
|
m = np.conj(m)
|
|
171
235
|
|
|
236
|
+
# Wiscombe's truncation: the series is summed through order n_terms
|
|
172
237
|
if n_pole == 0:
|
|
173
|
-
|
|
238
|
+
n_terms = int(x + 4.05 * x**0.33333 + 2.0)
|
|
174
239
|
else:
|
|
175
|
-
|
|
240
|
+
n_terms = n_pole
|
|
176
241
|
|
|
177
|
-
a = np.zeros(
|
|
178
|
-
b = np.zeros(
|
|
242
|
+
a = np.zeros(n_terms, dtype=np.complex128)
|
|
243
|
+
b = np.zeros(n_terms, dtype=np.complex128)
|
|
179
244
|
if x <= 0:
|
|
180
245
|
return a, b
|
|
181
246
|
|
|
182
247
|
inv_x = 1.0 / x
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
248
|
+
sin_x = np.sin(x)
|
|
249
|
+
cos_x = np.cos(x)
|
|
250
|
+
|
|
251
|
+
# psi comes from Miller's downwards recurrence. The upwards one is fine while
|
|
252
|
+
# n stays below x, which is all Wiscombe's truncation needs, but its relative
|
|
253
|
+
# accuracy is already down to about 1e-6 at n = n_terms and it collapses
|
|
254
|
+
# completely past that, so a caller asking for extra orders used to get noise.
|
|
255
|
+
psi = _psi_downwards_py(np.complex128(x), n_terms).real
|
|
256
|
+
|
|
257
|
+
# chi is the growing solution, so its upwards recurrence is the stable one.
|
|
258
|
+
# Building xi as psi + i*chi keeps xi exactly consistent with the psi above,
|
|
259
|
+
# which the lossless identity Re(a_n) == |a_n|**2 depends on.
|
|
260
|
+
chi_nm1 = cos_x
|
|
261
|
+
chi_n = cos_x * inv_x + sin_x
|
|
187
262
|
|
|
188
263
|
if m.real > 0.0:
|
|
189
|
-
D = _D_calc_py(m, x,
|
|
264
|
+
D = _D_calc_py(m, x, n_terms + 2)
|
|
190
265
|
|
|
191
|
-
for n in range(1,
|
|
266
|
+
for n in range(1, n_terms + 1):
|
|
192
267
|
n_over_x = n * inv_x
|
|
268
|
+
xi_nm1 = psi[n - 1] + 1j * chi_nm1
|
|
269
|
+
xi_n = psi[n] + 1j * chi_n
|
|
193
270
|
temp = D[n - 1] / m + n_over_x
|
|
194
|
-
a[n - 1] = (temp *
|
|
271
|
+
a[n - 1] = (temp * psi[n] - psi[n - 1]) / (temp * xi_n - xi_nm1)
|
|
195
272
|
temp = D[n - 1] * m + n_over_x
|
|
196
|
-
b[n - 1] = (temp *
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
xi_nm1 = xi_n
|
|
201
|
-
xi_n = xi
|
|
202
|
-
psi_nm1 = psi_n
|
|
203
|
-
psi_n = psi
|
|
273
|
+
b[n - 1] = (temp * psi[n] - psi[n - 1]) / (temp * xi_n - xi_nm1)
|
|
274
|
+
chi = (2 * n + 1) * inv_x * chi_n - chi_nm1
|
|
275
|
+
chi_nm1 = chi_n
|
|
276
|
+
chi_n = chi
|
|
204
277
|
|
|
205
278
|
else:
|
|
206
|
-
for n in range(1,
|
|
279
|
+
for n in range(1, n_terms + 1):
|
|
207
280
|
n_over_x = n * inv_x
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if n_pole != 0:
|
|
217
|
-
a = a[:-1]
|
|
218
|
-
b = b[:-1]
|
|
281
|
+
xi_nm1 = psi[n - 1] + 1j * chi_nm1
|
|
282
|
+
xi_n = psi[n] + 1j * chi_n
|
|
283
|
+
a[n - 1] = (n_over_x * psi[n] - psi[n - 1]) / (n_over_x * xi_n - xi_nm1)
|
|
284
|
+
b[n - 1] = psi[n] / xi_n
|
|
285
|
+
chi = (2 * n + 1) * inv_x * chi_n - chi_nm1
|
|
286
|
+
chi_nm1 = chi_n
|
|
287
|
+
chi_n = chi
|
|
219
288
|
|
|
220
289
|
return np.conjugate(a), np.conjugate(b)
|
|
221
290
|
|
|
@@ -237,55 +306,46 @@ def _cn_dn_py(m, x, n_pole):
|
|
|
237
306
|
m = np.conj(m)
|
|
238
307
|
mx = m * x
|
|
239
308
|
|
|
309
|
+
# same truncation as _an_bn_py so the internal and external series match
|
|
240
310
|
if n_pole == 0:
|
|
241
|
-
|
|
311
|
+
n_terms = int(x + 4.05 * x**0.33333 + 2.0)
|
|
242
312
|
else:
|
|
243
|
-
|
|
313
|
+
n_terms = n_pole
|
|
244
314
|
|
|
245
|
-
c = np.zeros(
|
|
246
|
-
d = np.zeros(
|
|
315
|
+
c = np.zeros(n_terms, dtype=np.complex128)
|
|
316
|
+
d = np.zeros(n_terms, dtype=np.complex128)
|
|
247
317
|
if x <= 0:
|
|
248
318
|
return c, d
|
|
249
319
|
|
|
250
320
|
inv_x = 1.0 / x
|
|
251
321
|
# no need to calculate anything when sphere is perfectly conducting
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
xi_nm1 = np.complex128(
|
|
261
|
-
xi_n = np.complex128(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
322
|
+
# (m.real <= 0 or an infinite index): there is no internal field, so c and d
|
|
323
|
+
# stay zero. The `and` chain must not be mixed with `or` here, otherwise the
|
|
324
|
+
# test is true for every finite index and m=0 divides by zero below.
|
|
325
|
+
if m.real > 0.0 and not np.isinf(m.real) and not np.isinf(m.imag):
|
|
326
|
+
sin_x = np.sin(x)
|
|
327
|
+
cos_x = np.cos(x)
|
|
328
|
+
|
|
329
|
+
# xi is the growing solution, so seeding it and running upwards is stable
|
|
330
|
+
xi_nm1 = np.complex128(sin_x + 1j * cos_x)
|
|
331
|
+
xi_n = np.complex128((sin_x * inv_x - cos_x) + 1j * (cos_x * inv_x + sin_x))
|
|
332
|
+
|
|
333
|
+
psi_x = _psi_downwards_py(np.complex128(x), n_terms + 1)
|
|
334
|
+
psi_mx = _psi_downwards_py(np.complex128(mx), n_terms + 1)
|
|
335
|
+
Dmx = _D_calc_down_py(np.complex128(mx), n_terms + 2)
|
|
336
|
+
Dx = _D_calc_down_py(np.complex128(x), n_terms + 2)
|
|
337
|
+
|
|
338
|
+
for n in range(1, n_terms + 1):
|
|
267
339
|
n_over_x = n * inv_x
|
|
268
|
-
common = (
|
|
340
|
+
common = (psi_x[n] / psi_mx[n]) * ((Dx[n - 1] + n_over_x) * xi_n - xi_nm1)
|
|
269
341
|
|
|
270
342
|
c[n - 1] = m * common / ((m * Dmx[n - 1] + n_over_x) * xi_n - xi_nm1)
|
|
271
343
|
d[n - 1] = common / ((Dmx[n - 1] / m + n_over_x) * xi_n - xi_nm1)
|
|
272
344
|
|
|
273
|
-
|
|
274
|
-
psi = two_np1 * inv_x * psi_n - psi_nm1
|
|
275
|
-
psi_nm1 = psi_n
|
|
276
|
-
psi_n = psi
|
|
277
|
-
|
|
278
|
-
psi_mx = two_np1 * inv_mx * psi_n_mx - psi_nm1_mx
|
|
279
|
-
psi_nm1_mx = psi_n_mx
|
|
280
|
-
psi_n_mx = psi_mx
|
|
281
|
-
|
|
282
|
-
xi = two_np1 * inv_x * xi_n - xi_nm1
|
|
345
|
+
xi = (2 * n + 1) * inv_x * xi_n - xi_nm1
|
|
283
346
|
xi_nm1 = xi_n
|
|
284
347
|
xi_n = xi
|
|
285
348
|
|
|
286
|
-
if n_pole != 0:
|
|
287
|
-
c = c[:-1]
|
|
288
|
-
d = d[:-1]
|
|
289
349
|
return np.conjugate(c), np.conjugate(d)
|
|
290
350
|
|
|
291
351
|
|
|
@@ -322,6 +382,9 @@ def _pi_tau_py(mu, pi, tau):
|
|
|
322
382
|
pi[n] = ((2 * n + 1) * mu * temp - (n + 1) * pi_nm2) / n
|
|
323
383
|
pi_nm2 = temp
|
|
324
384
|
|
|
385
|
+
# each pass sets tau one order behind pi, so the highest order is still due
|
|
386
|
+
tau[n_terms - 1] = n_terms * mu * pi[n_terms - 1] - (n_terms + 1) * pi_nm2
|
|
387
|
+
|
|
325
388
|
|
|
326
389
|
def _S1_S2_py(m, x, mu, n_pole):
|
|
327
390
|
"""
|
|
@@ -343,6 +406,9 @@ def _S1_S2_py(m, x, mu, n_pole):
|
|
|
343
406
|
"""
|
|
344
407
|
a, b = _an_bn_py(m, x, 0)
|
|
345
408
|
N = len(a)
|
|
409
|
+
if n_pole < 0 or n_pole > N:
|
|
410
|
+
raise ValueError("n_pole must be 0 (all terms) or a multipole order in 1.." + str(N))
|
|
411
|
+
|
|
346
412
|
pi = np.zeros(N)
|
|
347
413
|
tau = np.zeros(N)
|
|
348
414
|
scale = _series_scale_factors(N)
|
|
@@ -353,14 +419,17 @@ def _S1_S2_py(m, x, mu, n_pole):
|
|
|
353
419
|
S1 = np.zeros(nangles, dtype=np.complex128)
|
|
354
420
|
S2 = np.zeros(nangles, dtype=np.complex128)
|
|
355
421
|
|
|
422
|
+
# zero-based arrays: multipole order n lives at index n-1
|
|
423
|
+
j = n_pole - 1
|
|
424
|
+
|
|
356
425
|
for k in range(nangles):
|
|
357
426
|
_pi_tau_py(mu[k], pi, tau)
|
|
358
427
|
if n_pole == 0:
|
|
359
428
|
S1[k] = np.dot(pi, scale_a) + np.dot(tau, scale_b)
|
|
360
429
|
S2[k] = np.dot(tau, scale_a) + np.dot(pi, scale_b)
|
|
361
430
|
else:
|
|
362
|
-
S1[k] = scale[
|
|
363
|
-
S2[k] = scale[
|
|
431
|
+
S1[k] = scale[j] * (pi[j] * a[j] + tau[j] * b[j])
|
|
432
|
+
S2[k] = scale[j] * (tau[j] * a[j] + pi[j] * b[j])
|
|
364
433
|
|
|
365
434
|
return np.conjugate(S1), np.conjugate(S2)
|
|
366
435
|
|
|
@@ -457,7 +526,9 @@ def _single_sphere_py(m, x, n_pole, e_field):
|
|
|
457
526
|
m: the complex index of refraction of the sphere
|
|
458
527
|
x: the size parameter of the sphere
|
|
459
528
|
n_pole: a non-zero value returns the contribution by the n_pole multipole
|
|
460
|
-
e_field:
|
|
529
|
+
e_field: selects which multipole of order n_pole contributes, the
|
|
530
|
+
electric one a_n (True) or the magnetic one b_n (False).
|
|
531
|
+
Ignored when n_pole == 0.
|
|
461
532
|
|
|
462
533
|
Returns:
|
|
463
534
|
qext: the total extinction efficiency
|
|
@@ -465,7 +536,10 @@ def _single_sphere_py(m, x, n_pole, e_field):
|
|
|
465
536
|
qback: the backscatter efficiency
|
|
466
537
|
g: the average cosine of the scattering phase function
|
|
467
538
|
"""
|
|
468
|
-
|
|
539
|
+
# a sphere of zero size scatters nothing; every efficiency tends to zero,
|
|
540
|
+
# including qback, whose small-sphere form is 0/0 at x=0
|
|
541
|
+
if x <= 0:
|
|
542
|
+
return 0.0, 0.0, 0.0, 0.0
|
|
469
543
|
|
|
470
544
|
# case when sphere matches its environment
|
|
471
545
|
if abs(m.real - 1) <= 1e-8 and abs(m.imag) < 1e-8:
|
|
@@ -507,12 +581,18 @@ def _single_sphere_py(m, x, n_pole, e_field):
|
|
|
507
581
|
g = 4.0 * np.sum(asy1 + asy2) / qsca / x2
|
|
508
582
|
|
|
509
583
|
else:
|
|
584
|
+
# isolate one multipole: the electric term a_n or the magnetic term b_n
|
|
510
585
|
cn = 2.0 * n_pole + 1
|
|
511
|
-
|
|
512
|
-
qext = 2.0 * cn *
|
|
586
|
+
coeff = a[-1] if e_field else b[-1]
|
|
587
|
+
qext = 2.0 * cn * coeff.real / x2
|
|
588
|
+
# the (-1)**n_pole and the sign of b_n drop out of the modulus
|
|
589
|
+
qback = np.abs(cn * coeff) ** 2 / x2
|
|
513
590
|
qsca = qext
|
|
514
591
|
if m.imag < 0:
|
|
515
|
-
qsca = 2.0 * cn *
|
|
516
|
-
|
|
592
|
+
qsca = 2.0 * cn * np.abs(coeff) ** 2 / x2
|
|
593
|
+
|
|
594
|
+
# pi_n**2 and tau_n**2 are both even in mu, so an isolated multipole of
|
|
595
|
+
# one parity scatters symmetrically about 90 degrees and <cos(theta)>=0
|
|
596
|
+
g = 0.0
|
|
517
597
|
|
|
518
598
|
return qext, qsca, qback, g
|
miepython/monte_carlo.py
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Sample scattering angles from the Mie phase function.
|
|
3
|
+
|
|
4
|
+
Monte Carlo photon transport needs a random exit direction distributed according to
|
|
5
|
+
the phase function. This module builds the inverse-transform table that provides
|
|
6
|
+
one: ``mu_with_uniform_cdf`` returns angles spaced so that equal steps along the
|
|
7
|
+
cumulative distribution are equally likely, and ``generate_mie_costheta`` draws from
|
|
8
|
+
that table. ``docs/06_random_deviates.ipynb`` works through the whole procedure.
|
|
9
|
+
|
|
10
|
+
Importing
|
|
11
|
+
---------
|
|
12
|
+
|
|
13
|
+
This module is *not* imported with the package, because it imports ``miepython``
|
|
14
|
+
itself and doing so from ``miepython/__init__.py`` would be circular. Ask for it by
|
|
15
|
+
name::
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import miepython.monte_carlo as mc
|
|
19
|
+
|
|
20
|
+
m, x = 1.33, 2.0 # a water droplet
|
|
21
|
+
mu, cdf = mc.mu_with_uniform_cdf(m, x, 500) # 500-entry lookup table
|
|
22
|
+
|
|
23
|
+
rng = np.random.default_rng(12345)
|
|
24
|
+
cosines = [mc.generate_mie_costheta(mu, rng=rng) for _ in range(10000)]
|
|
25
|
+
|
|
26
|
+
Build the table once and reuse it: it costs a full Mie calculation, while each draw
|
|
27
|
+
off it costs one random number. The number of table entries sets how finely the
|
|
28
|
+
phase function is resolved, so strongly forward-scattering spheres need more of
|
|
29
|
+
them.
|
|
30
|
+
|
|
31
|
+
Passing ``rng`` keeps the draws reproducible without disturbing the process-wide
|
|
32
|
+
``numpy.random``; omitting it falls back to that global generator.
|
|
3
33
|
"""
|
|
4
34
|
|
|
5
35
|
import numpy as np
|
|
@@ -96,7 +126,7 @@ def mu_with_uniform_cdf(m, x, num):
|
|
|
96
126
|
return [mu, cdf_values]
|
|
97
127
|
|
|
98
128
|
|
|
99
|
-
def generate_mie_costheta(mu_cdf):
|
|
129
|
+
def generate_mie_costheta(mu_cdf, rng=None):
|
|
100
130
|
"""
|
|
101
131
|
Generate a new scattering angle using a cdf.
|
|
102
132
|
|
|
@@ -107,18 +137,37 @@ def generate_mie_costheta(mu_cdf):
|
|
|
107
137
|
|
|
108
138
|
Args:
|
|
109
139
|
mu_cdf: a cumulative distribution function
|
|
140
|
+
rng: optional source of randomness, anything with a ``random()`` method
|
|
141
|
+
such as ``numpy.random.default_rng()``. Omitting it uses the process
|
|
142
|
+
wide ``numpy.random``, whose stream a caller cannot control without
|
|
143
|
+
reseeding the whole program, so pass a generator when reproducibility
|
|
144
|
+
matters.
|
|
110
145
|
|
|
111
146
|
Returns:
|
|
112
147
|
The cosine of the scattering angle
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
Draw a reproducible angle without disturbing the global stream:
|
|
151
|
+
|
|
152
|
+
>>> import numpy as np
|
|
153
|
+
>>> import miepython.monte_carlo as mc
|
|
154
|
+
>>> mu, cdf = mc.mu_with_uniform_cdf(1.33, 1.7, 50)
|
|
155
|
+
>>> rng = np.random.default_rng(42)
|
|
156
|
+
>>> first = mc.generate_mie_costheta(mu, rng=rng)
|
|
157
|
+
>>> bool(-1.0 <= first <= 1.0)
|
|
158
|
+
True
|
|
159
|
+
>>> bool(first == mc.generate_mie_costheta(mu, rng=np.random.default_rng(42)))
|
|
160
|
+
True
|
|
113
161
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
if rng is None:
|
|
163
|
+
rng = np.random
|
|
164
|
+
|
|
165
|
+
# rng.random() draws from [0, 1), so int(r * num) never reaches num and
|
|
166
|
+
# index + 1 stays inside the table. No clamp is needed.
|
|
116
167
|
num = len(mu_cdf) - 1
|
|
117
|
-
index = int(
|
|
118
|
-
if index >= num:
|
|
119
|
-
index = num - 1
|
|
168
|
+
index = int(rng.random() * num)
|
|
120
169
|
|
|
121
170
|
x = mu_cdf[index]
|
|
122
|
-
x += (mu_cdf[index + 1] - mu_cdf[index]) *
|
|
171
|
+
x += (mu_cdf[index + 1] - mu_cdf[index]) * rng.random()
|
|
123
172
|
|
|
124
173
|
return x
|
miepython/rayleigh.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
+
from .core import normalization_factor as core_normalization_factor
|
|
6
|
+
|
|
5
7
|
__all__ = (
|
|
6
8
|
"efficiencies_mx",
|
|
7
9
|
"efficiencies",
|
|
@@ -62,7 +64,10 @@ def efficiencies(m, d, lambda0, n_env=1.0):
|
|
|
62
64
|
|
|
63
65
|
def normalization_factor(m, x, norm_str):
|
|
64
66
|
"""
|
|
65
|
-
Figure out scattering function normalization.
|
|
67
|
+
Figure out scattering function normalization for a small sphere.
|
|
68
|
+
|
|
69
|
+
The rules are the same as for the full Mie solution, so they live in
|
|
70
|
+
``miepython.core``; only the efficiencies feeding them are the Rayleigh ones.
|
|
66
71
|
|
|
67
72
|
Args:
|
|
68
73
|
m: complex index of refraction of sphere
|
|
@@ -72,38 +77,7 @@ def normalization_factor(m, x, norm_str):
|
|
|
72
77
|
Returns:
|
|
73
78
|
scaling factor needed for scattering function
|
|
74
79
|
"""
|
|
75
|
-
|
|
76
|
-
norm = norm_str.lower()
|
|
77
|
-
|
|
78
|
-
if norm in ["bohren"]:
|
|
79
|
-
factor = 1 / 2
|
|
80
|
-
|
|
81
|
-
elif norm in ["wiscombe"]:
|
|
82
|
-
factor = 1
|
|
83
|
-
|
|
84
|
-
elif norm in ["qsca", "scattering_efficiency"]:
|
|
85
|
-
factor = x * np.sqrt(np.pi)
|
|
86
|
-
|
|
87
|
-
else:
|
|
88
|
-
qext, qsca, _, _ = efficiencies_mx(m, x)
|
|
89
|
-
|
|
90
|
-
if norm in ["a", "albedo"]:
|
|
91
|
-
factor = x * np.sqrt(np.pi * qext)
|
|
92
|
-
|
|
93
|
-
if norm in ["1", "one", "unity"]:
|
|
94
|
-
factor = x * np.sqrt(qsca * np.pi)
|
|
95
|
-
|
|
96
|
-
if norm in ["four_pi", "4pi"]:
|
|
97
|
-
factor = x * np.sqrt(qsca / 4)
|
|
98
|
-
|
|
99
|
-
if norm in ["qext", "extinction_efficiency"]:
|
|
100
|
-
factor = x * np.sqrt(qsca * np.pi / qext)
|
|
101
|
-
|
|
102
|
-
if factor is None:
|
|
103
|
-
raise ValueError(
|
|
104
|
-
"normalization must be one of 'albedo' (default), 'one', '4pi', 'qext', 'qsca', 'bohren', or 'wiscombe'"
|
|
105
|
-
)
|
|
106
|
-
return factor
|
|
80
|
+
return core_normalization_factor(m, x, norm_str, efficiency_source=efficiencies_mx)
|
|
107
81
|
|
|
108
82
|
|
|
109
83
|
def S1_S2(m, x, mu, norm="albedo"):
|
miepython/util.py
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
__all__ = (
|
|
6
6
|
"cs_scalar",
|
|
7
7
|
"cs_vector",
|
|
8
8
|
"cs",
|
|
9
|
+
"phasor_str_scalar",
|
|
9
10
|
"phasor_str",
|
|
10
11
|
"cartesian_to_spherical",
|
|
11
12
|
"spherical_to_cartesian",
|
|
@@ -14,10 +15,29 @@ _all_ = (
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def cartesian_to_spherical(x, y, z):
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"""
|
|
19
|
+
Convert Cartesian coordinates (x, y, z) to spherical (r, theta, phi).
|
|
20
|
+
|
|
21
|
+
Scalars and arrays are both accepted and the three inputs are broadcast
|
|
22
|
+
against each other. At the origin theta is reported as 0, and z/r is clipped
|
|
23
|
+
so that rounding cannot push the arccos argument outside [-1, 1].
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
x: Cartesian x coordinate, scalar or array-like
|
|
27
|
+
y: Cartesian y coordinate, scalar or array-like
|
|
28
|
+
z: Cartesian z coordinate, scalar or array-like
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
r, theta, phi: radius, polar angle from +z, azimuth from +x toward +y
|
|
32
|
+
"""
|
|
33
|
+
xx, yy, zz = np.broadcast_arrays(np.asarray(x), np.asarray(y), np.asarray(z))
|
|
34
|
+
r = np.sqrt(xx**2 + yy**2 + zz**2)
|
|
35
|
+
|
|
36
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
37
|
+
cos_theta = np.where(r == 0, 1.0, np.clip(zz / r, -1.0, 1.0))
|
|
38
|
+
|
|
39
|
+
theta = np.arccos(cos_theta) # polar angle (0 to pi)
|
|
40
|
+
phi = np.arctan2(yy, xx) # azimuthal angle (-pi to pi)
|
|
21
41
|
return r, theta, phi
|
|
22
42
|
|
|
23
43
|
|
miepython/vsh.py
CHANGED
|
@@ -29,18 +29,27 @@ Bessel Function Selection:
|
|
|
29
29
|
|
|
30
30
|
Functions:
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
All four take the same arguments ``(n, lambda0, d_sphere, m_index, r, theta,
|
|
33
|
+
phi)``: the multipole order, the vacuum wavelength, the sphere diameter, the
|
|
34
|
+
refractive index of the medium the point lies in, and the spherical coordinates
|
|
35
|
+
of that point. ``lambda0``, ``d_sphere`` and ``r`` share one length unit.
|
|
36
|
+
|
|
37
|
+
M_odd(n, lambda0, d_sphere, m_index, r, theta, phi)
|
|
33
38
|
Compute the nth odd magnetic vector spherical harmonic (m=1).
|
|
34
39
|
|
|
35
|
-
M_even(n,
|
|
40
|
+
M_even(n, lambda0, d_sphere, m_index, r, theta, phi)
|
|
36
41
|
Compute the nth even magnetic vector spherical harmonic (m=1).
|
|
37
42
|
|
|
38
|
-
N_odd(n,
|
|
43
|
+
N_odd(n, lambda0, d_sphere, m_index, r, theta, phi)
|
|
39
44
|
Compute the nth odd electric vector spherical harmonic (m=1).
|
|
40
45
|
|
|
41
|
-
N_even(n,
|
|
46
|
+
N_even(n, lambda0, d_sphere, m_index, r, theta, phi)
|
|
42
47
|
Compute the nth even electric vector spherical harmonic (m=1).
|
|
43
48
|
|
|
49
|
+
Each has an ``*_array`` counterpart -- ``M_odd_array``, ``M_even_array``,
|
|
50
|
+
``N_odd_array``, ``N_even_array`` -- with the identical signature, taking
|
|
51
|
+
array-valued ``r``, ``theta`` and ``phi``.
|
|
52
|
+
|
|
44
53
|
Additional Utility Functions:
|
|
45
54
|
|
|
46
55
|
mie_pi(n, theta, deg=False)
|
|
@@ -54,8 +63,8 @@ Additional Utility Functions:
|
|
|
54
63
|
|
|
55
64
|
import numpy as np
|
|
56
65
|
from scipy.special import spherical_jn, lpmv, factorial2
|
|
57
|
-
from
|
|
58
|
-
|
|
66
|
+
from .bessel import spherical_h1, d_riccati_bessel_h1
|
|
67
|
+
from ._backend import D_calc
|
|
59
68
|
|
|
60
69
|
__all__ = (
|
|
61
70
|
"mie_tau",
|
|
@@ -312,7 +321,7 @@ def N_base(n, m_index, kr, theta, inside):
|
|
|
312
321
|
else:
|
|
313
322
|
factor1 = spherical_jn(n, rho)
|
|
314
323
|
# D_calc returns D_1..D_N; request one extra term and take D_n.
|
|
315
|
-
Dn =
|
|
324
|
+
Dn = D_calc(m_index, kr, n + 1)[n - 1]
|
|
316
325
|
factor2 = factor1 * Dn
|
|
317
326
|
factor1 /= rho
|
|
318
327
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: miepython
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: sphinx_automodapi; extra == "docs"
|
|
|
34
34
|
Requires-Dist: sphinx_rtd_theme; extra == "docs"
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
37
38
|
Requires-Dist: imageio; extra == "dev"
|
|
38
39
|
Requires-Dist: requests; extra == "dev"
|
|
39
40
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -72,7 +73,7 @@ Dynamic: license-file
|
|
|
72
73
|
:alt: DOI
|
|
73
74
|
|
|
74
75
|
.. |license| image:: https://img.shields.io/github/license/scottprahl/miepython?color=68CA66
|
|
75
|
-
:target: https://github.com/scottprahl/miepython/blob/
|
|
76
|
+
:target: https://github.com/scottprahl/miepython/blob/main/LICENSE.txt
|
|
76
77
|
:alt: License
|
|
77
78
|
|
|
78
79
|
.. |test| image:: https://github.com/scottprahl/miepython/actions/workflows/test.yaml/badge.svg
|
|
@@ -208,9 +209,9 @@ JIT Enabled 0.15 s 27×
|
|
|
208
209
|
Citation
|
|
209
210
|
--------
|
|
210
211
|
|
|
211
|
-
If you use ``
|
|
212
|
+
If you use ``miepython`` in academic, instructional, or applied technical work, please cite:
|
|
212
213
|
|
|
213
|
-
Prahl, S. (2026). *miepython: Pure python calculation of Mie scattering* (Version 3.
|
|
214
|
+
Prahl, S. (2026). *miepython: Pure python calculation of Mie scattering* (Version 3.2.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.7949263
|
|
214
215
|
|
|
215
216
|
BibTeX:
|
|
216
217
|
|
|
@@ -222,7 +223,7 @@ BibTeX:
|
|
|
222
223
|
url = {https://github.com/scottprahl/miepython},
|
|
223
224
|
doi = {10.5281/zenodo.7949263},
|
|
224
225
|
year = {2026},
|
|
225
|
-
version = {3.
|
|
226
|
+
version = {3.2.0},
|
|
226
227
|
publisher = {Zenodo}
|
|
227
228
|
}
|
|
228
229
|
|