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/mie_jit.py CHANGED
@@ -82,10 +82,79 @@ def _D_upwards(z, N, D):
82
82
  D[n] = 1 / (n / z - D[n - 1]) - n / z
83
83
 
84
84
 
85
+ @njit((complex128, int64), cache=True)
86
+ def _psi_downwards_nb(z, nstop):
87
+ """
88
+ Compute the Riccati-Bessel functions psi_0(z)..psi_nstop(z).
89
+
90
+ The three-term upwards recurrence
91
+ ``psi_{n+1} = (2n+1)/z psi_n - psi_{n-1}`` is contaminated by the growing
92
+ chi_n solution once n exceeds ``|z|``, which is the normal case for psi_n(mx)
93
+ whenever the relative index is below one. Miller's algorithm is used
94
+ instead: seed the recurrence well above ``|z|`` where psi_n is the decaying
95
+ solution, run it downwards, then fix the arbitrary scale against whichever
96
+ of psi_0 or psi_1 is larger. sin(z) and cos(z) cannot both be small, so one
97
+ of those two seeds is always well conditioned.
98
+
99
+ Args:
100
+ z: function argument (the refractive index times the size parameter)
101
+ nstop: highest order needed
102
+
103
+ Returns:
104
+ Array of psi_k(z) for k=0 to nstop
105
+ """
106
+ abs_z = abs(z)
107
+ n_start = int(max(float(nstop), abs_z)) + 25 + int(1.5 * np.sqrt(abs_z))
108
+
109
+ psi = np.zeros(n_start + 2, dtype=np.complex128)
110
+ psi[n_start] = 1e-50
111
+ for n in range(n_start, 0, -1):
112
+ psi[n - 1] = (2 * n + 1) / z * psi[n] - psi[n + 1]
113
+ if abs(psi[n - 1]) > 1e200: # keep the growing tail in range
114
+ for k in range(n - 1, n_start + 2):
115
+ psi[k] /= 1e200
116
+
117
+ sin_z = np.sin(z)
118
+ psi_0 = sin_z
119
+ psi_1 = sin_z / z - np.cos(z)
120
+ if abs(psi_1) > abs(psi_0):
121
+ scale = psi_1 / psi[1]
122
+ else:
123
+ scale = psi_0 / psi[0]
124
+ for k in range(n_start + 2):
125
+ psi[k] *= scale
126
+
127
+ return psi[: nstop + 1]
128
+
129
+
130
+ @njit((complex128, int64), cache=True)
131
+ def _D_calc_down_nb(z, N):
132
+ """
133
+ Compute D_1(z)..D_N(z) using only the downwards recurrence.
134
+
135
+ ``_D_calc_nb`` picks between the upwards and downwards recurrences with
136
+ Wiscombe's criterion, which considers the refractive index but not the number
137
+ of terms. The upwards recurrence loses accuracy once N exceeds ``|z|`` (about
138
+ 4 digits for a small sphere), and the resulting noise differs between the
139
+ numba and pure-python backends. The internal-field coefficients are not on
140
+ the hot path, so they always take the stable route.
141
+
142
+ Args:
143
+ z: function argument (already multiplied by the refractive index)
144
+ N: highest order needed
145
+
146
+ Returns:
147
+ Array of logarithmic derivatives D_k(z) for k=1 to N
148
+ """
149
+ D = np.zeros(N + 1, dtype=np.complex128)
150
+ _D_downwards(z, N, D)
151
+ return D[1:]
152
+
153
+
85
154
  @njit((complex128, float64, int64), cache=True)
86
155
  def _D_calc_nb(m, x, N):
87
156
  """
88
- Compute the logarithmic derivative of ψ_n(z) using the best method.
157
+ Compute the logarithmic derivative of ψ_n(z) using the downwards recurrence.
89
158
 
90
159
  D_n(z) = d[log ψ_n(z)] = ψ_n'(z)/ψ_n(z)
91
160
 
@@ -94,6 +163,14 @@ def _D_calc_nb(m, x, N):
94
163
 
95
164
  The zero-based array, D[:], is shifted so that D[0] = D₁(z) = ψ₁'(z)/ψ₁(z)
96
165
 
166
+ Wiscombe's criterion used to pick between the upwards and downwards
167
+ recurrences from the refractive index alone. The upwards recurrence is
168
+ contaminated once N passes ``|mx|``, which is the case for every small sphere,
169
+ and it left the Mie coefficients wrong by as much as 1% near m=1. The
170
+ downwards recurrence is stable everywhere, and the criterion already chose it
171
+ for most large spheres, so always taking it costs little. ``_D_upwards`` is
172
+ kept for comparison and testing.
173
+
97
174
  Args:
98
175
  m: the np.complex128 index of refraction of the sphere
99
176
  x: the size parameter of the sphere
@@ -102,20 +179,15 @@ def _D_calc_nb(m, x, N):
102
179
  Returns:
103
180
  Array of logarithmic derivatives D_k(z) for k=1 to N-1.
104
181
  """
105
- n = m.real
106
- kappa = np.abs(m.imag)
107
- D = np.zeros(N + 1, dtype=np.complex128)
108
- mx = np.complex128(m * x) # ensure complex
109
-
110
- if n < 1 or n > 10 or kappa > 10 or x * kappa >= 3.9 - 10.8 * n + 13.78 * n**2:
111
- _D_downwards(mx, N, D)
112
- else:
113
- _D_upwards(mx, N, D)
114
- return D[1:]
182
+ return _D_calc_down_nb(np.complex128(m * x), N)
115
183
 
116
184
 
117
- @njit((complex128, float64, int64), cache=True, fastmath=True)
118
- def _an_bn_nb(m, x, n_pole):
185
+ # Lazily compiled, unlike the kernels around it, so that n_pole can carry the same
186
+ # default as ``_an_bn_py``: numba rejects an omitted argument against an eager
187
+ # signature, which used to make ``an_bn(m, x)`` work with the JIT off and raise
188
+ # TypeError with it on.
189
+ @njit(cache=True)
190
+ def _an_bn_nb(m, x, n_pole=0):
119
191
  """
120
192
  Compute arrays of Mie coefficients a_n and b_n for a sphere.
121
193
 
@@ -123,8 +195,8 @@ def _an_bn_nb(m, x, n_pole):
123
195
  formula. The length of the arrays is chosen so that the error when the series
124
196
  is summed is around 1e-6.
125
197
 
126
- If n_pole>0, then the array sizes will be n_pole+1. This is useful when
127
- trying to isolate the behavior of a particular multipole.
198
+ If n_pole>0, then the arrays hold exactly n_pole terms, orders 1 to n_pole.
199
+ This is useful when trying to isolate the behavior of a particular multipole.
128
200
 
129
201
  To support resonance calculations, one can specify the number of terms
130
202
  to be calculated. In general, using too few or too many terms increases the
@@ -137,7 +209,8 @@ def _an_bn_nb(m, x, n_pole):
137
209
  n_pole: the number of An and Bn terms (0 does autosizing)
138
210
 
139
211
  Returns:
140
- a, b: arrays of Mie coefficents An and Bn
212
+ a, b: arrays of Mie coefficents An and Bn, one entry per order,
213
+ with no padding. ``_cn_dn`` returns the same number of terms.
141
214
  """
142
215
  # make sure sign of imaginary part is negative
143
216
  m_re = m.real
@@ -146,55 +219,70 @@ def _an_bn_nb(m, x, n_pole):
146
219
  m_im = -m_im
147
220
  m = complex(m_re, m_im)
148
221
 
149
- # make sure array size is large enough
222
+ # Wiscombe's truncation: the series is summed through order n_terms
150
223
  if n_pole == 0:
151
- nstop = int(x + 4.05 * x**0.33333 + 2.0) + 1
224
+ n_terms = int(x + 4.05 * x**0.33333 + 2.0)
152
225
  else:
153
- nstop = n_pole + 1
226
+ n_terms = n_pole
154
227
 
155
- a = np.zeros(nstop, dtype=np.complex128)
156
- b = np.zeros(nstop, dtype=np.complex128)
228
+ a = np.zeros(n_terms, dtype=np.complex128)
229
+ b = np.zeros(n_terms, dtype=np.complex128)
157
230
  if x <= 0:
158
231
  return a, b
159
232
 
160
- psi_nm1 = np.sin(x) # nm1 = n-1 = 0
161
- psi_n = psi_nm1 / x - np.cos(x)
162
- xi_nm1 = np.complex128(psi_nm1 + 1j * np.cos(x))
163
- xi_n = np.complex128(psi_n + 1j * (np.cos(x) / x + np.sin(x)))
233
+ # group the arithmetic exactly as _an_bn_py does: multiplying by a precomputed
234
+ # 1/x here and dividing by x there differed by one ulp, which the recurrences
235
+ # amplified into 2.6e-7 between the two backends.
236
+ inv_x = 1.0 / x
237
+ sin_x = np.sin(x)
238
+ cos_x = np.cos(x)
239
+
240
+ # psi comes from Miller's downwards recurrence. The upwards one is fine while
241
+ # n stays below x, which is all Wiscombe's truncation needs, but its relative
242
+ # accuracy is already down to about 1e-6 at n = n_terms and it collapses
243
+ # completely past that, so a caller asking for extra orders used to get noise.
244
+ psi = _psi_downwards_nb(np.complex128(x), n_terms).real
245
+
246
+ # chi is the growing solution, so its upwards recurrence is the stable one.
247
+ # Building xi as psi + i*chi keeps xi exactly consistent with the psi above,
248
+ # which the lossless identity Re(a_n) == |a_n|**2 depends on.
249
+ chi_nm1 = cos_x
250
+ chi_n = cos_x * inv_x + sin_x
164
251
 
165
252
  if m.real > 0.0:
166
- D = _D_calc_nb(m, x, nstop + 1)
167
-
168
- for n in range(1, nstop):
169
- temp = D[n - 1] / m + n / x
170
- a[n - 1] = (temp * psi_n - psi_nm1) / (temp * xi_n - xi_nm1)
171
- temp = D[n - 1] * m + n / x
172
- b[n - 1] = (temp * psi_n - psi_nm1) / (temp * xi_n - xi_nm1)
173
- psi = (2 * n + 1) * psi_n / x - psi_nm1
174
- xi = (2 * n + 1) * xi_n / x - xi_nm1
175
- xi_nm1 = xi_n
176
- xi_n = xi
177
- psi_nm1 = psi_n
178
- psi_n = psi
253
+ D = _D_calc_nb(m, x, n_terms + 2)
254
+
255
+ for n in range(1, n_terms + 1):
256
+ n_over_x = n * inv_x
257
+ xi_nm1 = psi[n - 1] + 1j * chi_nm1
258
+ xi_n = psi[n] + 1j * chi_n
259
+ temp = D[n - 1] / m + n_over_x
260
+ a[n - 1] = (temp * psi[n] - psi[n - 1]) / (temp * xi_n - xi_nm1)
261
+ temp = D[n - 1] * m + n_over_x
262
+ b[n - 1] = (temp * psi[n] - psi[n - 1]) / (temp * xi_n - xi_nm1)
263
+ chi = (2 * n + 1) * inv_x * chi_n - chi_nm1
264
+ chi_nm1 = chi_n
265
+ chi_n = chi
179
266
 
180
267
  else:
181
- for n in range(1, nstop):
182
- a[n - 1] = (n * psi_n / x - psi_nm1) / (n * xi_n / x - xi_nm1)
183
- b[n - 1] = psi_n / xi_n
184
- xi = (2 * n + 1) * xi_n / x - xi_nm1
185
- xi_nm1 = xi_n
186
- xi_n = xi
187
- psi_nm1 = psi_n
188
- psi_n = xi_n.real
189
-
190
- if n_pole != 0:
191
- a = a[:-1]
192
- b = b[:-1]
268
+ for n in range(1, n_terms + 1):
269
+ n_over_x = n * inv_x
270
+ xi_nm1 = psi[n - 1] + 1j * chi_nm1
271
+ xi_n = psi[n] + 1j * chi_n
272
+ a[n - 1] = (n_over_x * psi[n] - psi[n - 1]) / (n_over_x * xi_n - xi_nm1)
273
+ b[n - 1] = psi[n] / xi_n
274
+ chi = (2 * n + 1) * inv_x * chi_n - chi_nm1
275
+ chi_nm1 = chi_n
276
+ chi_n = chi
193
277
 
194
278
  return np.conjugate(a), np.conjugate(b)
195
279
 
196
280
 
197
- @njit((complex128, float64, int64), fastmath=True)
281
+ # No kernel in this module uses fastmath. It implies LLVM's nnan and ninf, which
282
+ # folded the np.isinf guard below to False and let an infinite index divide by
283
+ # zero, and its reassociation also broke the exact Re(a_n) == |a_n|**2 identity
284
+ # that a lossless sphere has to satisfy. Do not add it back.
285
+ @njit((complex128, float64, int64))
198
286
  def _cn_dn_nb(m, x, n_pole):
199
287
  """
200
288
  Calculate Mie coefficients c_n and d_n for the internal field of a sphere.
@@ -215,51 +303,48 @@ def _cn_dn_nb(m, x, n_pole):
215
303
  m = complex(m_re, m_im)
216
304
  mx = m * x
217
305
 
306
+ # same truncation as _an_bn_nb so the internal and external series match
218
307
  if n_pole == 0:
219
- nstop = int(x + 4.05 * x**0.33333 + 2.0) + 1
308
+ n_terms = int(x + 4.05 * x**0.33333 + 2.0)
220
309
  else:
221
- nstop = n_pole + 1
310
+ n_terms = n_pole
222
311
 
223
- c = np.zeros(nstop, dtype=np.complex128)
224
- d = np.zeros(nstop, dtype=np.complex128)
312
+ c = np.zeros(n_terms, dtype=np.complex128)
313
+ d = np.zeros(n_terms, dtype=np.complex128)
225
314
  if x <= 0:
226
315
  return c, d
227
316
 
228
317
  # no need to calculate anything when sphere is perfectly conducting
229
- if m.real > 0.0 and not np.isinf(m.real) or not np.isinf(m.imag):
230
- psi_nm1 = np.sin(x) # nm1 = n-1 = 0
231
- psi_n = psi_nm1 / x - np.cos(x)
232
-
233
- psi_nm1_mx = np.sin(mx) # nm1 = n-1 = 0
234
- psi_n_mx = psi_nm1_mx / mx - np.cos(mx)
235
-
236
- xi_nm1 = np.complex128(psi_nm1 + 1j * np.cos(x))
237
- xi_n = np.complex128(psi_n + 1j * (np.cos(x) / x + np.sin(x)))
238
-
239
- Dmx = _D_calc_nb(np.complex128(m), x, nstop + 1)
240
- Dx = _D_calc_nb(np.complex128(1), x, nstop + 1)
241
-
242
- for n in range(1, nstop + 1):
243
- common = (psi_n / psi_n_mx) * ((Dx[n - 1] + n / x) * xi_n - xi_nm1)
244
-
245
- c[n - 1] = m * common / ((m * Dmx[n - 1] + n / x) * xi_n - xi_nm1)
246
- d[n - 1] = common / ((Dmx[n - 1] / m + n / x) * xi_n - xi_nm1)
247
-
248
- psi = (2 * n + 1) * psi_n / x - psi_nm1
249
- psi_nm1 = psi_n
250
- psi_n = psi
251
-
252
- psi_mx = (2 * n + 1) * psi_n_mx / mx - psi_nm1_mx
253
- psi_nm1_mx = psi_n_mx
254
- psi_n_mx = psi_mx
255
-
256
- xi = (2 * n + 1) * xi_n / x - xi_nm1
318
+ # (m.real <= 0 or an infinite index): there is no internal field, so c and d
319
+ # stay zero. The `and` chain must not be mixed with `or` here, otherwise the
320
+ # test is true for every finite index and m=0 divides by zero below.
321
+ if m.real > 0.0 and not np.isinf(m.real) and not np.isinf(m.imag):
322
+ # same arithmetic grouping as _cn_dn_py, so the two backends agree to the
323
+ # last bits rather than only to about 1e-13
324
+ inv_x = 1.0 / x
325
+ sin_x = np.sin(x)
326
+ cos_x = np.cos(x)
327
+
328
+ # xi is the growing solution, so seeding it and running upwards is stable
329
+ xi_nm1 = np.complex128(sin_x + 1j * cos_x)
330
+ xi_n = np.complex128((sin_x * inv_x - cos_x) + 1j * (cos_x * inv_x + sin_x))
331
+
332
+ psi_x = _psi_downwards_nb(np.complex128(x), n_terms + 1)
333
+ psi_mx = _psi_downwards_nb(np.complex128(mx), n_terms + 1)
334
+ Dmx = _D_calc_down_nb(np.complex128(mx), n_terms + 2)
335
+ Dx = _D_calc_down_nb(np.complex128(x), n_terms + 2)
336
+
337
+ for n in range(1, n_terms + 1):
338
+ n_over_x = n * inv_x
339
+ common = (psi_x[n] / psi_mx[n]) * ((Dx[n - 1] + n_over_x) * xi_n - xi_nm1)
340
+
341
+ c[n - 1] = m * common / ((m * Dmx[n - 1] + n_over_x) * xi_n - xi_nm1)
342
+ d[n - 1] = common / ((Dmx[n - 1] / m + n_over_x) * xi_n - xi_nm1)
343
+
344
+ xi = (2 * n + 1) * inv_x * xi_n - xi_nm1
257
345
  xi_nm1 = xi_n
258
346
  xi_n = xi
259
347
 
260
- if n_pole != 0:
261
- c = c[:-1]
262
- d = d[:-1]
263
348
  return np.conjugate(c), np.conjugate(d)
264
349
 
265
350
 
@@ -297,6 +382,9 @@ def _pi_tau_nb(mu, pi, tau):
297
382
  pi[n] = ((2 * n + 1) * mu * temp - (n + 1) * pi_nm2) / n
298
383
  pi_nm2 = temp
299
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
+
300
388
 
301
389
  @njit((complex128, float64, float64[:], int64), cache=True)
302
390
  def _S1_S2_nb(m, x, mu, n_pole):
@@ -319,6 +407,9 @@ def _S1_S2_nb(m, x, mu, n_pole):
319
407
  """
320
408
  a, b = _an_bn_nb(m, x, 0)
321
409
  N = len(a)
410
+ if n_pole < 0 or n_pole > N:
411
+ raise ValueError("n_pole must be 0 (all terms) or a multipole order in 1.." + str(N))
412
+
322
413
  pi = np.zeros(N)
323
414
  tau = np.zeros(N)
324
415
  n = np.arange(1, N + 1, dtype=np.float64)
@@ -328,6 +419,9 @@ def _S1_S2_nb(m, x, mu, n_pole):
328
419
  S1 = np.zeros(nangles, dtype=np.complex128)
329
420
  S2 = np.zeros(nangles, dtype=np.complex128)
330
421
 
422
+ # zero-based arrays: multipole order n lives at index n-1
423
+ j = n_pole - 1
424
+
331
425
  for k in range(nangles):
332
426
  _pi_tau_nb(mu[k], pi, tau)
333
427
  if n_pole == 0:
@@ -340,13 +434,13 @@ def _S1_S2_nb(m, x, mu, n_pole):
340
434
  S1[k] = s1
341
435
  S2[k] = s2
342
436
  else:
343
- S1[k] = scale[n_pole] * (pi[n_pole] * a[n_pole] + tau[n_pole] * b[n_pole])
344
- S2[k] = scale[n_pole] * (tau[n_pole] * a[n_pole] + pi[n_pole] * b[n_pole])
437
+ S1[k] = scale[j] * (pi[j] * a[j] + tau[j] * b[j])
438
+ S2[k] = scale[j] * (tau[j] * a[j] + pi[j] * b[j])
345
439
 
346
440
  return np.conjugate(S1), np.conjugate(S2)
347
441
 
348
442
 
349
- @njit((complex128, float64), cache=True, fastmath=True)
443
+ @njit((complex128, float64), cache=True)
350
444
  def _small_conducting_sphere_nb(_m, x):
351
445
  """
352
446
  Calculate the efficiencies for a small conducting spheres.
@@ -384,7 +478,7 @@ def _small_conducting_sphere_nb(_m, x):
384
478
  return qext, qsca, qback, g
385
479
 
386
480
 
387
- @njit((complex128, float64), cache=True, fastmath=True)
481
+ @njit((complex128, float64), cache=True)
388
482
  def _small_sphere_nb(m, x):
389
483
  """
390
484
  Calculate the efficiencies for a small sphere.
@@ -432,7 +526,7 @@ def _small_sphere_nb(m, x):
432
526
  return qext, qsca, qback, g
433
527
 
434
528
 
435
- @njit((complex128, float64, int64, boolean), cache=True, fastmath=True)
529
+ @njit((complex128, float64, int64, boolean), cache=True)
436
530
  def _single_sphere_nb(m, x, n_pole, e_field):
437
531
  """
438
532
  Calculate the efficiencies for a single sphere (both m and x are scalars).
@@ -441,7 +535,9 @@ def _single_sphere_nb(m, x, n_pole, e_field):
441
535
  m: the complex index of refraction of the sphere
442
536
  x: the size parameter of the sphere
443
537
  n_pole: a non-zero value returns the contribution by the n_pole multipole
444
- e_field: Electric (True) or Magnetic Field otherwise
538
+ e_field: selects which multipole of order n_pole contributes, the
539
+ electric one a_n (True) or the magnetic one b_n (False).
540
+ Ignored when n_pole == 0.
445
541
 
446
542
  Returns:
447
543
  qext: the total extinction efficiency
@@ -449,7 +545,10 @@ def _single_sphere_nb(m, x, n_pole, e_field):
449
545
  qback: the backscatter efficiency
450
546
  g: the average cosine of the scattering phase function
451
547
  """
452
- _ = e_field # unused
548
+ # a sphere of zero size scatters nothing; every efficiency tends to zero,
549
+ # including qback, whose small-sphere form is 0/0 at x=0
550
+ if x <= 0:
551
+ return 0.0, 0.0, 0.0, 0.0
453
552
 
454
553
  # case when sphere matches its environment
455
554
  if abs(m.real - 1) <= 1e-8 and abs(m.imag) < 1e-8:
@@ -515,12 +614,18 @@ def _single_sphere_nb(m, x, n_pole, e_field):
515
614
  g = 4.0 * g_acc / qsca / x2
516
615
 
517
616
  else:
617
+ # isolate one multipole: the electric term a_n or the magnetic term b_n
518
618
  cn = 2.0 * n_pole + 1
519
- qext = 2.0 * cn * (a[-1].real + b[-1].real) / x2
520
- qback = np.abs((-1) ** n_pole * cn * (a[-1] - b[-1])) ** 2 / x2
619
+ coeff = a[-1] if e_field else b[-1]
620
+ qext = 2.0 * cn * coeff.real / x2
621
+ # the (-1)**n_pole and the sign of b_n drop out of the modulus
622
+ qback = np.abs(cn * coeff) ** 2 / x2
521
623
  qsca = qext
522
624
  if m.imag < 0:
523
- qsca = 2.0 * cn * (np.abs(a[-1]) ** 2 + np.abs(b[-1]) ** 2) / x2
524
- g = 0
625
+ qsca = 2.0 * cn * np.abs(coeff) ** 2 / x2
626
+
627
+ # pi_n**2 and tau_n**2 are both even in mu, so an isolated multipole of
628
+ # one parity scatters symmetrically about 90 degrees and <cos(theta)>=0
629
+ g = 0.0
525
630
 
526
631
  return qext, qsca, qback, g