miepython 3.2.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 CHANGED
@@ -42,6 +42,29 @@ The scattering matrix
42
42
 
43
43
  mie.phase_matrix(m, x, mu)
44
44
 
45
+ Submodules
46
+ ----------
47
+
48
+ ``miepython.rayleigh`` is imported with the package and needs nothing beyond NumPy.
49
+ It mirrors the API above using the small-sphere limit, which is handy for checking
50
+ the full solution as x goes to zero::
51
+
52
+ mie.rayleigh.efficiencies_mx(1.5, 0.01)
53
+
54
+ ``miepython.vsh`` holds the vector spherical harmonics as a direct transcription of
55
+ the textbook formulas. It is a reference for development and cross-checking rather
56
+ than a fast path, it requires the optional ``scipy`` dependency, and so it is bound
57
+ only when scipy is installed::
58
+
59
+ mie.vsh.M_odd(1, k, d_sphere, r, theta, phi)
60
+
61
+ ``miepython.monte_carlo`` samples scattering angles from the Mie phase function. It
62
+ imports this package, so importing it from here would be circular; ask for it
63
+ explicitly instead::
64
+
65
+ import miepython.monte_carlo as mc
66
+ mu, cdf = mc.mu_with_uniform_cdf(m, x, 100)
67
+
45
68
  Near-field calculations are provided by the ``miepython.field`` module::
46
69
 
47
70
  from miepython.field import e_near, h_near, eh_near
@@ -68,6 +91,8 @@ from ._backend import single_sphere, small_conducting_sphere, small_sphere
68
91
  from .core import efficiencies, intensities, i_par, i_per, i_unpolarized
69
92
  from .core import efficiencies_mx, S1_S2, phase_matrix, coefficients
70
93
 
94
+ from . import rayleigh
95
+
71
96
  _FIELD_IMPORT_ERROR = None
72
97
 
73
98
 
@@ -93,10 +118,14 @@ def _missing_field_function(name):
93
118
 
94
119
  try:
95
120
  from . import field as _field
121
+ from . import vsh # noqa: F401 reachable as miepython.vsh; needs scipy, so guarded here
96
122
  except ModuleNotFoundError as exc:
97
123
  missing_module = str(getattr(exc, "name", "") or "")
98
124
  if missing_module.startswith("scipy"):
99
125
  _FIELD_IMPORT_ERROR = exc
126
+ # ``miepython.vsh`` is left unbound rather than stubbed out. It is a
127
+ # reference implementation used for development and cross-checking, not part
128
+ # of the public surface, and every one of its functions needs scipy.
100
129
  e_far = _missing_field_function("e_far")
101
130
  e_near = _missing_field_function("e_near")
102
131
  h_near = _missing_field_function("h_near")
@@ -115,8 +144,10 @@ else:
115
144
  h_near_cartesian = _field.h_near_cartesian
116
145
  eh_near_cartesian = _field.eh_near_cartesian
117
146
 
118
- # functions exposed to the user
147
+ # Names exposed to the user. ``vsh`` is deliberately absent: it is bound only when
148
+ # scipy is installed, and a star-import must not depend on that.
119
149
  __all__ = (
150
+ "rayleigh",
120
151
  "intensities",
121
152
  "i_par",
122
153
  "i_per",
@@ -147,7 +178,7 @@ __all__ = (
147
178
  "_D_downwards",
148
179
  )
149
180
 
150
- __version__ = "3.2.0"
181
+ __version__ = "3.3.1"
151
182
  __author__ = "Scott Prahl"
152
183
  __email__ = "scott.prahl@oit.edu"
153
184
  __copyright__ = "2017-2026, Scott Prahl"
miepython/core.py CHANGED
@@ -47,9 +47,77 @@ __all__ = (
47
47
  "S1_S2",
48
48
  "phase_matrix",
49
49
  "coefficients",
50
+ "wiscombe_terms",
51
+ "normalization_factor",
50
52
  )
51
53
 
52
54
 
55
+ def wiscombe_terms(x):
56
+ """
57
+ Return the number of Mie series terms from Wiscombe's criterion.
58
+
59
+ The series is truncated after this many orders so that the summed scattering
60
+ quantities are accurate to about 1e-6. The low-level ``an_bn`` and ``cn_dn``
61
+ kernels repeat this expression because numba cannot call back into Python.
62
+
63
+ Args:
64
+ x: the size parameter of the sphere
65
+
66
+ Returns:
67
+ number of terms, so orders 1 through the returned value
68
+ """
69
+ return int(x + 4.05 * x**0.33333 + 2.0)
70
+
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
+
53
121
  def coefficients(m, x, n_pole=0, internal=False):
54
122
  """
55
123
  Computes the Mie coefficients for a sphere.
@@ -73,31 +141,42 @@ def coefficients(m, x, n_pole=0, internal=False):
73
141
  calculate fields inside the sphere.
74
142
 
75
143
  Returns:
76
- tuple:
77
- - a (complex or array-like): The computed Mie coefficient A_n or an array of A_n values.
78
- - b (complex or array-like): The computed Mie coefficient B_n or an array of B_n values.
144
+ ndarray: ``[a, b]``, or ``[a, b, c, d]`` when ``internal`` is True. For
145
+ scalar ``m`` and ``x`` each entry is a 1-D array of coefficients, so
146
+ the result has shape ``(2, n_terms)`` or ``(4, n_terms)``. For array
147
+ input each entry gains a leading axis over the spheres, giving
148
+ ``(2, n_spheres, n_pole)`` or ``(4, n_spheres, n_pole)``.
79
149
 
80
150
  Notes:
81
151
  - If the imaginary part of the refractive index is positive, it is
82
152
  automatically corrected to its conjugate value to ensure a valid input.
153
+ - Every returned coefficient is a real term; the arrays carry no padding.
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.
83
158
 
84
159
  Examples:
85
160
  Compute coefficients for a single sphere:
86
- >>> m = 1.5 - 0.1j
87
- >>> x = 0.1
88
- >>> mie_coefficients(m, x)
89
- (array([3.33370015e-05+1.97363100e-04j, 1.77504613e-08+1.11834113e-07j,
90
- 4.60529820e-12+2.97368373e-11j, 1.58460985e-28+1.25881287e-14j]),
91
- array([6.67296256e-08+2.75469444e-07j, 1.90474848e-11+7.86835968e-11j,
92
- 3.02615454e-15+1.24937186e-14j, 1.58460985e-28+1.25881287e-14j]))
93
-
94
- Compute first multipoles for multiple spheres:
95
- >>> m = [1.5 + 0.1j, 1.4 + 0.05j]
96
- >>> x = [2.0, 1.8]
97
- >>> mie_coefficients(m, x, 1)
98
- (array([0.44794644+0.38665192j, 0.27813728+0.38495241j]),
99
- array([0.5621083 +0.25504616j, 0.20206531+0.29589842j]))
161
+
162
+ >>> import miepython as mie
163
+ >>> a, b = mie.coefficients(1.5 - 0.1j, 0.1)
164
+ >>> len(a), len(b)
165
+ (3, 3)
166
+ >>> print(f"{a[0]:.6e}")
167
+ 3.333700e-05-1.973631e-04j
168
+ >>> print(f"{b[0]:.6e}")
169
+ 6.672963e-08-2.754694e-07j
170
+
171
+ Keep only the dipole term for each of several spheres:
172
+
173
+ >>> a, b = mie.coefficients([1.5 - 0.1j, 1.4 - 0.05j], [2.0, 1.8], n_pole=1)
174
+ >>> a.shape
175
+ (2, 1)
176
+ >>> print(f"{a[0, 0]:.6f}")
177
+ 0.447946-0.386652j
100
178
  """
179
+ _check_m_x(m, x)
101
180
  mlen = np.size(m) if np.ndim(m) > 0 else 0
102
181
  xlen = np.size(x) if np.ndim(x) > 0 else 0
103
182
 
@@ -155,8 +234,11 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
155
234
  - If 0 (default), computes contributions from all multipoles.
156
235
  - If non-zero, computes contributions from the specified multipole order.
157
236
  e_field (boolean):
158
- If n_pole>0, then True value returns the electric
159
- multipole contribution otherwise the Magnetic field contribution
237
+ When n_pole>0, selects which multipole of that order contributes:
238
+ True gives the electric multipole a_n, False the magnetic
239
+ multipole b_n. The two add up to the full order-n_pole
240
+ contribution for qext and qsca. Ignored when n_pole is 0.
241
+
160
242
  Returns:
161
243
  tuple:
162
244
  qext (float or array-like):
@@ -170,7 +252,8 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
170
252
  scattered in the exact backward direction (theta = 180 degrees).
171
253
  g (float or array-like):
172
254
  Asymmetry parameter, representing the average cosine of the scattering
173
- angle over all angles.
255
+ angle over all angles. This is exactly zero when n_pole > 0, because a
256
+ single multipole of one parity scatters symmetrically about 90 degrees.
174
257
 
175
258
  Notes:
176
259
  - Ensure m and x have compatible dimensions if passed as arrays.
@@ -179,21 +262,32 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
179
262
  - This implementation assumes spherical, homogeneous particles in a
180
263
  non-absorbing medium.
181
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
+
182
269
  Examples:
183
- Compute efficiencies for a sphere with fixed parameters:
270
+ Compute efficiencies for one sphere:
184
271
 
185
272
  >>> import miepython as mie
186
- >>> mie.efficiencies(1.5 - 0.01j, 2.0, 0)
187
- (qext: 1.81, qsca: 1.72, qback: 0.27, g: 0.63)
273
+ >>> qext, qsca, qback, g = mie.efficiencies_mx(1.5 - 0.01j, 2.0)
274
+ >>> print(f"{qext:.6f} {qsca:.6f} {qback:.6f} {g:.6f}")
275
+ 1.812597 1.724396 0.266214 0.630214
188
276
 
189
- Compute efficiencies for wavelength-dependent refractive indices:
277
+ Compute efficiencies for a wavelength-dependent refractive index:
190
278
 
191
- >>> import miepython as mie
279
+ >>> import numpy as np
192
280
  >>> m_array = np.array([1.5 - 0.01j, 1.45 - 0.02j])
193
281
  >>> x_array = np.array([2.0, 2.5])
194
- >>> mie.efficiencies(m_array, x_array, 0)
195
- (qext: [1.81, 2.15], qsca: [1.72, 1.95], qback: [0.27, 0.31], g: [0.63, 0.70])
282
+ >>> qext, qsca, qback, g = mie.efficiencies_mx(m_array, x_array)
283
+ >>> np.round(qext, 6)
284
+ array([1.812597, 2.14972 ])
285
+
286
+ Use :func:`efficiencies` instead when the diameter and wavelength are
287
+ known rather than the size parameter.
196
288
  """
289
+ _check_m_x(m, x)
290
+
197
291
  # ensure imaginary part of refractive index is negative
198
292
  if np.isscalar(m):
199
293
  m = np.conj(m) if np.imag(m) > 0 else m
@@ -234,7 +328,12 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
234
328
  return qext, qsca, qback, g
235
329
 
236
330
 
237
- def normalization_factor(m, x, norm_str):
331
+ def _mie_efficiencies(m, x):
332
+ """Return the full Mie efficiencies, the default source for normalization."""
333
+ return single_sphere(m, x, 0, True)
334
+
335
+
336
+ def normalization_factor(m, x, norm_str, efficiency_source=_mie_efficiencies):
238
337
  """
239
338
  Figure out scattering function normalization.
240
339
 
@@ -242,6 +341,10 @@ def normalization_factor(m, x, norm_str):
242
341
  m: complex index of refraction of sphere
243
342
  x: dimensionless sphere size
244
343
  norm_str: string describing type of normalization
344
+ efficiency_source: callable returning ``(qext, qsca, qback, g)`` for ``m``
345
+ and ``x``. Defaults to the full Mie solution; ``miepython.rayleigh``
346
+ passes its own so that the two share these rules rather than keeping
347
+ a second copy of them.
245
348
 
246
349
  Returns:
247
350
  scaling factor needed for scattering function
@@ -259,7 +362,16 @@ def normalization_factor(m, x, norm_str):
259
362
  factor = x * np.sqrt(np.pi)
260
363
 
261
364
  else:
262
- qext, qsca, _, _ = single_sphere(m, x, 0, True)
365
+ qext, qsca, _, _ = efficiency_source(m, x)
366
+
367
+ # every remaining choice divides by qext or qsca
368
+ if qext <= 0 or qsca <= 0:
369
+ raise ValueError(
370
+ "normalization %r needs a sphere that scatters, but qext=%g and qsca=%g. "
371
+ "A sphere of zero size, or one whose index matches its surroundings, has "
372
+ "no scattering to normalize against. Use norm='wiscombe', 'bohren' or "
373
+ "'qsca' if an unnormalized amplitude is what you want." % (norm_str, qext, qsca)
374
+ )
263
375
 
264
376
  if norm in ["a", "albedo"]:
265
377
  factor = x * np.sqrt(np.pi * qext)
@@ -297,21 +409,34 @@ def S1_S2(m, x, mu, norm="albedo", n_pole=0):
297
409
  Args:
298
410
  m: the complex index of refraction of the sphere
299
411
  x: the size parameter of the sphere
300
- mu: the angles, cos(theta), to calculate scattering amplitudes
412
+ mu: the angles, cos(theta), to calculate scattering amplitudes. A scalar,
413
+ list, tuple or array is accepted; a scalar is treated as one angle,
414
+ so the returned amplitudes are still length-one arrays.
301
415
  norm: (optional) string describing scattering function normalization
302
- n_pole: return n_pole term from series (default=0 means include all terms)
416
+ n_pole: isolate a single multipole order, 1=dipole, 2=quadrupole,
417
+ 3=octupole (default=0 means include all terms). Summing over
418
+ every order reproduces the full series, so n_pole uses the same
419
+ convention as `efficiencies_mx`. Orders beyond the truncated
420
+ series raise a ValueError.
303
421
 
304
422
  Returns:
305
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.
306
429
  """
430
+ _check_m_x(m, x)
431
+ _check_one_sphere(m, x)
307
432
  if np.imag(m) > 0: # ensure imaginary part of refractive index is negative
308
433
  m = np.conj(m)
309
434
 
310
- if np.isscalar(mu):
311
- mu_array = np.array([mu], dtype=float)
312
- S1, S2 = _S1_S2(m, x, mu_array, n_pole)
313
- else:
314
- S1, S2 = _S1_S2(m, x, mu, n_pole)
435
+ # The numba kernel is declared for float64[:], so a list or an integer array
436
+ # would be rejected there while numpy accepted it. Coercing here keeps both
437
+ # backends callable with exactly the same arguments.
438
+ mu_array = np.atleast_1d(np.asarray(mu, dtype=float))
439
+ S1, S2 = _S1_S2(m, x, mu_array, n_pole)
315
440
 
316
441
  normalization = normalization_factor(m, x, norm)
317
442
 
@@ -345,7 +470,9 @@ def phase_matrix(m, x, mu, norm="albedo", n_pole=0):
345
470
  Args:
346
471
  m: the complex index of refraction of the sphere
347
472
  x: the size parameter of the sphere
348
- mu: the angles, cos(theta), of the phase scattering matrix
473
+ mu: the angles, cos(theta), of the phase scattering matrix. Unlike the
474
+ other angular functions this one squeezes a single angle away, so a
475
+ scalar gives a bare 4x4 matrix rather than 4x4x1.
349
476
  n_pole: return n_pole term from series (default=0 means include all terms)
350
477
  norm: (optional) string describing scattering function normalization
351
478
 
@@ -390,7 +517,9 @@ def i_per(m, x, mu, norm="albedo", n_pole=0):
390
517
  Args:
391
518
  m (complex): Complex index of refraction of the sphere.
392
519
  x (float): Size parameter of the sphere.
393
- mu (array-like): Cosine of the scattering angle(s), cos(θ), for which intensity is desired.
520
+ mu (array-like): Cosine of the scattering angle(s), cos(θ), for which
521
+ intensity is desired. A scalar counts as one angle and yields a
522
+ length-one array, not a bare number.
394
523
  norm (str, optional): Normalization method for the scattered intensity. Default is 'albedo'.
395
524
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
396
525
  default is 0, which returns the sum of all terms.
@@ -419,7 +548,9 @@ def i_par(m, x, mu, norm="albedo", n_pole=0):
419
548
  Args:
420
549
  m (complex): Complex index of refraction of the sphere.
421
550
  x (float): Size parameter of the sphere.
422
- mu (array-like): Cosine of the scattering angle(s), cos(θ), for which intensity is desired.
551
+ mu (array-like): Cosine of the scattering angle(s), cos(θ), for which
552
+ intensity is desired. A scalar counts as one angle and yields a
553
+ length-one array, not a bare number.
423
554
  norm (str, optional): Normalization method for the scattered intensity. Default is 'albedo'.
424
555
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
425
556
  default is 0, which returns the sum of all terms.
@@ -449,7 +580,8 @@ def i_unpolarized(m, x, mu, norm="albedo", n_pole=0):
449
580
  Args:
450
581
  m: the complex index of refraction of the sphere
451
582
  x: the size parameter
452
- mu: the cos(theta) of each direction desired
583
+ mu: the cos(theta) of each direction desired. A scalar counts as one
584
+ direction and yields a length-one array, not a bare number.
453
585
  norm: (optional) string describing scattering function normalization
454
586
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
455
587
  default is 0, which returns the sum of all terms.