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 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.0"
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,28 @@ __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
+
53
72
  def coefficients(m, x, n_pole=0, internal=False):
54
73
  """
55
74
  Computes the Mie coefficients for a sphere.
@@ -73,30 +92,36 @@ def coefficients(m, x, n_pole=0, internal=False):
73
92
  calculate fields inside the sphere.
74
93
 
75
94
  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.
95
+ ndarray: ``[a, b]``, or ``[a, b, c, d]`` when ``internal`` is True. For
96
+ scalar ``m`` and ``x`` each entry is a 1-D array of coefficients, so
97
+ the result has shape ``(2, n_terms)`` or ``(4, n_terms)``. For array
98
+ input each entry gains a leading axis over the spheres, giving
99
+ ``(2, n_spheres, n_pole)`` or ``(4, n_spheres, n_pole)``.
79
100
 
80
101
  Notes:
81
102
  - If the imaginary part of the refractive index is positive, it is
82
103
  automatically corrected to its conjugate value to ensure a valid input.
104
+ - Every returned coefficient is a real term; the arrays carry no padding.
83
105
 
84
106
  Examples:
85
107
  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]))
108
+
109
+ >>> import miepython as mie
110
+ >>> a, b = mie.coefficients(1.5 - 0.1j, 0.1)
111
+ >>> len(a), len(b)
112
+ (3, 3)
113
+ >>> print(f"{a[0]:.6e}")
114
+ 3.333700e-05-1.973631e-04j
115
+ >>> print(f"{b[0]:.6e}")
116
+ 6.672963e-08-2.754694e-07j
117
+
118
+ Keep only the dipole term for each of several spheres:
119
+
120
+ >>> a, b = mie.coefficients([1.5 - 0.1j, 1.4 - 0.05j], [2.0, 1.8], n_pole=1)
121
+ >>> a.shape
122
+ (2, 1)
123
+ >>> print(f"{a[0, 0]:.6f}")
124
+ 0.447946-0.386652j
100
125
  """
101
126
  mlen = np.size(m) if np.ndim(m) > 0 else 0
102
127
  xlen = np.size(x) if np.ndim(x) > 0 else 0
@@ -155,8 +180,11 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
155
180
  - If 0 (default), computes contributions from all multipoles.
156
181
  - If non-zero, computes contributions from the specified multipole order.
157
182
  e_field (boolean):
158
- If n_pole>0, then True value returns the electric
159
- multipole contribution otherwise the Magnetic field contribution
183
+ When n_pole>0, selects which multipole of that order contributes:
184
+ True gives the electric multipole a_n, False the magnetic
185
+ multipole b_n. The two add up to the full order-n_pole
186
+ contribution for qext and qsca. Ignored when n_pole is 0.
187
+
160
188
  Returns:
161
189
  tuple:
162
190
  qext (float or array-like):
@@ -170,7 +198,8 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
170
198
  scattered in the exact backward direction (theta = 180 degrees).
171
199
  g (float or array-like):
172
200
  Asymmetry parameter, representing the average cosine of the scattering
173
- angle over all angles.
201
+ angle over all angles. This is exactly zero when n_pole > 0, because a
202
+ single multipole of one parity scatters symmetrically about 90 degrees.
174
203
 
175
204
  Notes:
176
205
  - Ensure m and x have compatible dimensions if passed as arrays.
@@ -180,19 +209,24 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
180
209
  non-absorbing medium.
181
210
 
182
211
  Examples:
183
- Compute efficiencies for a sphere with fixed parameters:
212
+ Compute efficiencies for one sphere:
184
213
 
185
214
  >>> 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)
215
+ >>> qext, qsca, qback, g = mie.efficiencies_mx(1.5 - 0.01j, 2.0)
216
+ >>> print(f"{qext:.6f} {qsca:.6f} {qback:.6f} {g:.6f}")
217
+ 1.812597 1.724396 0.266214 0.630214
188
218
 
189
- Compute efficiencies for wavelength-dependent refractive indices:
219
+ Compute efficiencies for a wavelength-dependent refractive index:
190
220
 
191
- >>> import miepython as mie
221
+ >>> import numpy as np
192
222
  >>> m_array = np.array([1.5 - 0.01j, 1.45 - 0.02j])
193
223
  >>> 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])
224
+ >>> qext, qsca, qback, g = mie.efficiencies_mx(m_array, x_array)
225
+ >>> np.round(qext, 6)
226
+ array([1.812597, 2.14972 ])
227
+
228
+ Use :func:`efficiencies` instead when the diameter and wavelength are
229
+ known rather than the size parameter.
196
230
  """
197
231
  # ensure imaginary part of refractive index is negative
198
232
  if np.isscalar(m):
@@ -234,7 +268,12 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
234
268
  return qext, qsca, qback, g
235
269
 
236
270
 
237
- def normalization_factor(m, x, norm_str):
271
+ def _mie_efficiencies(m, x):
272
+ """Return the full Mie efficiencies, the default source for normalization."""
273
+ return single_sphere(m, x, 0, True)
274
+
275
+
276
+ def normalization_factor(m, x, norm_str, efficiency_source=_mie_efficiencies):
238
277
  """
239
278
  Figure out scattering function normalization.
240
279
 
@@ -242,6 +281,10 @@ def normalization_factor(m, x, norm_str):
242
281
  m: complex index of refraction of sphere
243
282
  x: dimensionless sphere size
244
283
  norm_str: string describing type of normalization
284
+ efficiency_source: callable returning ``(qext, qsca, qback, g)`` for ``m``
285
+ and ``x``. Defaults to the full Mie solution; ``miepython.rayleigh``
286
+ passes its own so that the two share these rules rather than keeping
287
+ a second copy of them.
245
288
 
246
289
  Returns:
247
290
  scaling factor needed for scattering function
@@ -259,7 +302,16 @@ def normalization_factor(m, x, norm_str):
259
302
  factor = x * np.sqrt(np.pi)
260
303
 
261
304
  else:
262
- qext, qsca, _, _ = single_sphere(m, x, 0, True)
305
+ qext, qsca, _, _ = efficiency_source(m, x)
306
+
307
+ # every remaining choice divides by qext or qsca
308
+ if qext <= 0 or qsca <= 0:
309
+ raise ValueError(
310
+ "normalization %r needs a sphere that scatters, but qext=%g and qsca=%g. "
311
+ "A sphere of zero size, or one whose index matches its surroundings, has "
312
+ "no scattering to normalize against. Use norm='wiscombe', 'bohren' or "
313
+ "'qsca' if an unnormalized amplitude is what you want." % (norm_str, qext, qsca)
314
+ )
263
315
 
264
316
  if norm in ["a", "albedo"]:
265
317
  factor = x * np.sqrt(np.pi * qext)
@@ -297,9 +349,15 @@ def S1_S2(m, x, mu, norm="albedo", n_pole=0):
297
349
  Args:
298
350
  m: the complex index of refraction of the sphere
299
351
  x: the size parameter of the sphere
300
- mu: the angles, cos(theta), to calculate scattering amplitudes
352
+ mu: the angles, cos(theta), to calculate scattering amplitudes. A scalar,
353
+ list, tuple or array is accepted; a scalar is treated as one angle,
354
+ so the returned amplitudes are still length-one arrays.
301
355
  norm: (optional) string describing scattering function normalization
302
- n_pole: return n_pole term from series (default=0 means include all terms)
356
+ n_pole: isolate a single multipole order, 1=dipole, 2=quadrupole,
357
+ 3=octupole (default=0 means include all terms). Summing over
358
+ every order reproduces the full series, so n_pole uses the same
359
+ convention as `efficiencies_mx`. Orders beyond the truncated
360
+ series raise a ValueError.
303
361
 
304
362
  Returns:
305
363
  S1, S2: the scattering amplitudes at each angle mu [sr**(-0.5)]
@@ -307,11 +365,11 @@ def S1_S2(m, x, mu, norm="albedo", n_pole=0):
307
365
  if np.imag(m) > 0: # ensure imaginary part of refractive index is negative
308
366
  m = np.conj(m)
309
367
 
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)
368
+ # The numba kernel is declared for float64[:], so a list or an integer array
369
+ # would be rejected there while numpy accepted it. Coercing here keeps both
370
+ # backends callable with exactly the same arguments.
371
+ mu_array = np.atleast_1d(np.asarray(mu, dtype=float))
372
+ S1, S2 = _S1_S2(m, x, mu_array, n_pole)
315
373
 
316
374
  normalization = normalization_factor(m, x, norm)
317
375
 
@@ -345,7 +403,9 @@ def phase_matrix(m, x, mu, norm="albedo", n_pole=0):
345
403
  Args:
346
404
  m: the complex index of refraction of the sphere
347
405
  x: the size parameter of the sphere
348
- mu: the angles, cos(theta), of the phase scattering matrix
406
+ mu: the angles, cos(theta), of the phase scattering matrix. Unlike the
407
+ other angular functions this one squeezes a single angle away, so a
408
+ scalar gives a bare 4x4 matrix rather than 4x4x1.
349
409
  n_pole: return n_pole term from series (default=0 means include all terms)
350
410
  norm: (optional) string describing scattering function normalization
351
411
 
@@ -390,7 +450,9 @@ def i_per(m, x, mu, norm="albedo", n_pole=0):
390
450
  Args:
391
451
  m (complex): Complex index of refraction of the sphere.
392
452
  x (float): Size parameter of the sphere.
393
- mu (array-like): Cosine of the scattering angle(s), cos(θ), for which intensity is desired.
453
+ mu (array-like): Cosine of the scattering angle(s), cos(θ), for which
454
+ intensity is desired. A scalar counts as one angle and yields a
455
+ length-one array, not a bare number.
394
456
  norm (str, optional): Normalization method for the scattered intensity. Default is 'albedo'.
395
457
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
396
458
  default is 0, which returns the sum of all terms.
@@ -419,7 +481,9 @@ def i_par(m, x, mu, norm="albedo", n_pole=0):
419
481
  Args:
420
482
  m (complex): Complex index of refraction of the sphere.
421
483
  x (float): Size parameter of the sphere.
422
- mu (array-like): Cosine of the scattering angle(s), cos(θ), for which intensity is desired.
484
+ mu (array-like): Cosine of the scattering angle(s), cos(θ), for which
485
+ intensity is desired. A scalar counts as one angle and yields a
486
+ length-one array, not a bare number.
423
487
  norm (str, optional): Normalization method for the scattered intensity. Default is 'albedo'.
424
488
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
425
489
  default is 0, which returns the sum of all terms.
@@ -449,7 +513,8 @@ def i_unpolarized(m, x, mu, norm="albedo", n_pole=0):
449
513
  Args:
450
514
  m: the complex index of refraction of the sphere
451
515
  x: the size parameter
452
- mu: the cos(theta) of each direction desired
516
+ mu: the cos(theta) of each direction desired. A scalar counts as one
517
+ direction and yields a length-one array, not a bare number.
453
518
  norm: (optional) string describing scattering function normalization
454
519
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
455
520
  default is 0, which returns the sum of all terms.