miepython 3.1.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
@@ -62,50 +85,69 @@ Example near-field usage::
62
85
 
63
86
  """
64
87
 
65
- import os
66
-
67
- USE_JIT = os.environ.get("MIEPYTHON_USE_JIT", "0") == "1"
68
-
69
- if USE_JIT:
70
- # pull in the jit‐compiled backends under the "private" names core.py expects
71
- from .mie_jit import _an_bn_nb as an_bn
72
- from .mie_jit import _cn_dn_nb as cn_dn
73
- from .mie_jit import _single_sphere_nb as single_sphere
74
- from .mie_jit import _small_sphere_nb as small_sphere
75
- from .mie_jit import _small_conducting_sphere_nb as small_conducting_sphere
76
- from .mie_jit import _pi_tau_nb as _pi_tau
77
- from .mie_jit import _D_calc_nb as _D_calc
78
- from .mie_jit import _S1_S2_nb as _S1_S2
79
- from .mie_jit import _Lentz_Dn
80
- from .mie_jit import _D_upwards
81
- from .mie_jit import _D_downwards
82
- else:
83
- from .mie_nojit import _an_bn_py as an_bn
84
- from .mie_nojit import _cn_dn_py as cn_dn
85
- from .mie_nojit import _single_sphere_py as single_sphere
86
- from .mie_nojit import _small_sphere_py as small_sphere
87
- from .mie_nojit import _small_conducting_sphere_py as small_conducting_sphere
88
- from .mie_nojit import _pi_tau_py as _pi_tau
89
- from .mie_nojit import _D_calc_py as _D_calc
90
- from .mie_nojit import _S1_S2_py as _S1_S2
91
- from .mie_nojit import _Lentz_Dn
92
- from .mie_nojit import _D_upwards
93
- from .mie_nojit import _D_downwards
88
+ from ._backend import D_calc, USE_JIT, _D_downwards, _D_upwards, _Lentz_Dn, _S1_S2, an_bn, cn_dn, pi_tau
89
+ from ._backend import single_sphere, small_conducting_sphere, small_sphere
94
90
 
95
91
  from .core import efficiencies, intensities, i_par, i_per, i_unpolarized
96
92
  from .core import efficiencies_mx, S1_S2, phase_matrix, coefficients
97
- from .field import (
98
- e_far,
99
- e_near,
100
- h_near,
101
- eh_near,
102
- e_near_cartesian,
103
- h_near_cartesian,
104
- eh_near_cartesian,
105
- )
106
93
 
107
- # functions exposed to the user
94
+ from . import rayleigh
95
+
96
+ _FIELD_IMPORT_ERROR = None
97
+
98
+
99
+ def _raise_field_import_error():
100
+ """Raise a consistent error when optional near-field dependencies are missing."""
101
+ raise ModuleNotFoundError(
102
+ "Near-field functions require the optional dependency 'scipy'. "
103
+ "Install miepython with SciPy support to use e_far/e_near/h_near/eh_near."
104
+ ) from _FIELD_IMPORT_ERROR
105
+
106
+
107
+ def _missing_field_function(name):
108
+ """Create a placeholder near-field function when SciPy is unavailable."""
109
+
110
+ def _missing(*_args, **_kwargs):
111
+ _raise_field_import_error()
112
+
113
+ _missing.__name__ = name
114
+ _missing.__qualname__ = name
115
+ _missing.__doc__ = f"{name} requires the optional dependency 'scipy'."
116
+ return _missing
117
+
118
+
119
+ try:
120
+ from . import field as _field
121
+ from . import vsh # noqa: F401 reachable as miepython.vsh; needs scipy, so guarded here
122
+ except ModuleNotFoundError as exc:
123
+ missing_module = str(getattr(exc, "name", "") or "")
124
+ if missing_module.startswith("scipy"):
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.
129
+ e_far = _missing_field_function("e_far")
130
+ e_near = _missing_field_function("e_near")
131
+ h_near = _missing_field_function("h_near")
132
+ eh_near = _missing_field_function("eh_near")
133
+ e_near_cartesian = _missing_field_function("e_near_cartesian")
134
+ h_near_cartesian = _missing_field_function("h_near_cartesian")
135
+ eh_near_cartesian = _missing_field_function("eh_near_cartesian")
136
+ else:
137
+ raise
138
+ else:
139
+ e_far = _field.e_far
140
+ e_near = _field.e_near
141
+ h_near = _field.h_near
142
+ eh_near = _field.eh_near
143
+ e_near_cartesian = _field.e_near_cartesian
144
+ h_near_cartesian = _field.h_near_cartesian
145
+ eh_near_cartesian = _field.eh_near_cartesian
146
+
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.
108
149
  __all__ = (
150
+ "rayleigh",
109
151
  "intensities",
110
152
  "i_par",
111
153
  "i_per",
@@ -127,15 +169,16 @@ __all__ = (
127
169
  "single_sphere",
128
170
  "small_sphere",
129
171
  "small_conducting_sphere",
172
+ "USE_JIT",
173
+ "D_calc",
174
+ "pi_tau",
130
175
  "_S1_S2",
131
- "_D_calc",
132
- "_pi_tau",
133
176
  "_Lentz_Dn",
134
177
  "_D_upwards",
135
178
  "_D_downwards",
136
179
  )
137
180
 
138
- __version__ = "3.1.0"
181
+ __version__ = "3.3.0"
139
182
  __author__ = "Scott Prahl"
140
183
  __email__ = "scott.prahl@oit.edu"
141
184
  __copyright__ = "2017-2026, Scott Prahl"
miepython/_backend.py ADDED
@@ -0,0 +1,49 @@
1
+ """Select and expose the active low-level backend implementation.
2
+
3
+ This module centralizes JIT vs no-JIT backend selection so other modules can
4
+ import backend callables without importing ``miepython`` package root.
5
+ """
6
+
7
+ import os
8
+
9
+ USE_JIT = os.environ.get("MIEPYTHON_USE_JIT", "0") == "1"
10
+
11
+ if USE_JIT:
12
+ from .mie_jit import _D_calc_nb as D_calc
13
+ from .mie_jit import _D_downwards
14
+ from .mie_jit import _D_upwards
15
+ from .mie_jit import _Lentz_Dn
16
+ from .mie_jit import _S1_S2_nb as _S1_S2
17
+ from .mie_jit import _an_bn_nb as an_bn
18
+ from .mie_jit import _cn_dn_nb as cn_dn
19
+ from .mie_jit import _pi_tau_nb as pi_tau
20
+ from .mie_jit import _single_sphere_nb as single_sphere
21
+ from .mie_jit import _small_conducting_sphere_nb as small_conducting_sphere
22
+ from .mie_jit import _small_sphere_nb as small_sphere
23
+ else:
24
+ from .mie_nojit import _D_calc_py as D_calc
25
+ from .mie_nojit import _D_downwards
26
+ from .mie_nojit import _D_upwards
27
+ from .mie_nojit import _Lentz_Dn
28
+ from .mie_nojit import _S1_S2_py as _S1_S2
29
+ from .mie_nojit import _an_bn_py as an_bn
30
+ from .mie_nojit import _cn_dn_py as cn_dn
31
+ from .mie_nojit import _pi_tau_py as pi_tau
32
+ from .mie_nojit import _single_sphere_py as single_sphere
33
+ from .mie_nojit import _small_conducting_sphere_py as small_conducting_sphere
34
+ from .mie_nojit import _small_sphere_py as small_sphere
35
+
36
+ __all__ = (
37
+ "USE_JIT",
38
+ "D_calc",
39
+ "an_bn",
40
+ "cn_dn",
41
+ "pi_tau",
42
+ "single_sphere",
43
+ "small_sphere",
44
+ "small_conducting_sphere",
45
+ "_S1_S2",
46
+ "_Lentz_Dn",
47
+ "_D_upwards",
48
+ "_D_downwards",
49
+ )
miepython/core.py CHANGED
@@ -34,8 +34,7 @@ Normalized Mie scattering intensities for angles mu=cos(theta)::
34
34
  """
35
35
 
36
36
  import numpy as np
37
- from miepython import an_bn, cn_dn, single_sphere, _S1_S2
38
-
37
+ from ._backend import _S1_S2, an_bn, cn_dn, single_sphere
39
38
 
40
39
  # not really needed but included for clarity
41
40
  __all__ = (
@@ -48,9 +47,28 @@ __all__ = (
48
47
  "S1_S2",
49
48
  "phase_matrix",
50
49
  "coefficients",
50
+ "wiscombe_terms",
51
+ "normalization_factor",
51
52
  )
52
53
 
53
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
+
54
72
  def coefficients(m, x, n_pole=0, internal=False):
55
73
  """
56
74
  Computes the Mie coefficients for a sphere.
@@ -74,30 +92,36 @@ def coefficients(m, x, n_pole=0, internal=False):
74
92
  calculate fields inside the sphere.
75
93
 
76
94
  Returns:
77
- tuple:
78
- - a (complex or array-like): The computed Mie coefficient A_n or an array of A_n values.
79
- - 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)``.
80
100
 
81
101
  Notes:
82
102
  - If the imaginary part of the refractive index is positive, it is
83
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.
84
105
 
85
106
  Examples:
86
107
  Compute coefficients for a single sphere:
87
- >>> m = 1.5 - 0.1j
88
- >>> x = 0.1
89
- >>> mie_coefficients(m, x)
90
- (array([3.33370015e-05+1.97363100e-04j, 1.77504613e-08+1.11834113e-07j,
91
- 4.60529820e-12+2.97368373e-11j, 1.58460985e-28+1.25881287e-14j]),
92
- array([6.67296256e-08+2.75469444e-07j, 1.90474848e-11+7.86835968e-11j,
93
- 3.02615454e-15+1.24937186e-14j, 1.58460985e-28+1.25881287e-14j]))
94
-
95
- Compute first multipoles for multiple spheres:
96
- >>> m = [1.5 + 0.1j, 1.4 + 0.05j]
97
- >>> x = [2.0, 1.8]
98
- >>> mie_coefficients(m, x, 1)
99
- (array([0.44794644+0.38665192j, 0.27813728+0.38495241j]),
100
- 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
101
125
  """
102
126
  mlen = np.size(m) if np.ndim(m) > 0 else 0
103
127
  xlen = np.size(x) if np.ndim(x) > 0 else 0
@@ -156,8 +180,11 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
156
180
  - If 0 (default), computes contributions from all multipoles.
157
181
  - If non-zero, computes contributions from the specified multipole order.
158
182
  e_field (boolean):
159
- If n_pole>0, then True value returns the electric
160
- 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
+
161
188
  Returns:
162
189
  tuple:
163
190
  qext (float or array-like):
@@ -171,7 +198,8 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
171
198
  scattered in the exact backward direction (theta = 180 degrees).
172
199
  g (float or array-like):
173
200
  Asymmetry parameter, representing the average cosine of the scattering
174
- 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.
175
203
 
176
204
  Notes:
177
205
  - Ensure m and x have compatible dimensions if passed as arrays.
@@ -181,19 +209,24 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
181
209
  non-absorbing medium.
182
210
 
183
211
  Examples:
184
- Compute efficiencies for a sphere with fixed parameters:
212
+ Compute efficiencies for one sphere:
185
213
 
186
214
  >>> import miepython as mie
187
- >>> mie.efficiencies(1.5 - 0.01j, 2.0, 0)
188
- (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
189
218
 
190
- Compute efficiencies for wavelength-dependent refractive indices:
219
+ Compute efficiencies for a wavelength-dependent refractive index:
191
220
 
192
- >>> import miepython as mie
221
+ >>> import numpy as np
193
222
  >>> m_array = np.array([1.5 - 0.01j, 1.45 - 0.02j])
194
223
  >>> x_array = np.array([2.0, 2.5])
195
- >>> mie.efficiencies(m_array, x_array, 0)
196
- (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.
197
230
  """
198
231
  # ensure imaginary part of refractive index is negative
199
232
  if np.isscalar(m):
@@ -235,7 +268,12 @@ def efficiencies_mx(m, x, n_pole=0, e_field=True):
235
268
  return qext, qsca, qback, g
236
269
 
237
270
 
238
- 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):
239
277
  """
240
278
  Figure out scattering function normalization.
241
279
 
@@ -243,6 +281,10 @@ def normalization_factor(m, x, norm_str):
243
281
  m: complex index of refraction of sphere
244
282
  x: dimensionless sphere size
245
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.
246
288
 
247
289
  Returns:
248
290
  scaling factor needed for scattering function
@@ -260,7 +302,16 @@ def normalization_factor(m, x, norm_str):
260
302
  factor = x * np.sqrt(np.pi)
261
303
 
262
304
  else:
263
- 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
+ )
264
315
 
265
316
  if norm in ["a", "albedo"]:
266
317
  factor = x * np.sqrt(np.pi * qext)
@@ -298,9 +349,15 @@ def S1_S2(m, x, mu, norm="albedo", n_pole=0):
298
349
  Args:
299
350
  m: the complex index of refraction of the sphere
300
351
  x: the size parameter of the sphere
301
- 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.
302
355
  norm: (optional) string describing scattering function normalization
303
- 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.
304
361
 
305
362
  Returns:
306
363
  S1, S2: the scattering amplitudes at each angle mu [sr**(-0.5)]
@@ -308,11 +365,11 @@ def S1_S2(m, x, mu, norm="albedo", n_pole=0):
308
365
  if np.imag(m) > 0: # ensure imaginary part of refractive index is negative
309
366
  m = np.conj(m)
310
367
 
311
- if np.isscalar(mu):
312
- mu_array = np.array([mu], dtype=float)
313
- S1, S2 = _S1_S2(m, x, mu_array, n_pole)
314
- else:
315
- 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)
316
373
 
317
374
  normalization = normalization_factor(m, x, norm)
318
375
 
@@ -346,7 +403,9 @@ def phase_matrix(m, x, mu, norm="albedo", n_pole=0):
346
403
  Args:
347
404
  m: the complex index of refraction of the sphere
348
405
  x: the size parameter of the sphere
349
- 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.
350
409
  n_pole: return n_pole term from series (default=0 means include all terms)
351
410
  norm: (optional) string describing scattering function normalization
352
411
 
@@ -391,7 +450,9 @@ def i_per(m, x, mu, norm="albedo", n_pole=0):
391
450
  Args:
392
451
  m (complex): Complex index of refraction of the sphere.
393
452
  x (float): Size parameter of the sphere.
394
- 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.
395
456
  norm (str, optional): Normalization method for the scattered intensity. Default is 'albedo'.
396
457
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
397
458
  default is 0, which returns the sum of all terms.
@@ -420,7 +481,9 @@ def i_par(m, x, mu, norm="albedo", n_pole=0):
420
481
  Args:
421
482
  m (complex): Complex index of refraction of the sphere.
422
483
  x (float): Size parameter of the sphere.
423
- 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.
424
487
  norm (str, optional): Normalization method for the scattered intensity. Default is 'albedo'.
425
488
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
426
489
  default is 0, which returns the sum of all terms.
@@ -450,7 +513,8 @@ def i_unpolarized(m, x, mu, norm="albedo", n_pole=0):
450
513
  Args:
451
514
  m: the complex index of refraction of the sphere
452
515
  x: the size parameter
453
- 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.
454
518
  norm: (optional) string describing scattering function normalization
455
519
  n_pole (int, optional): If greater than zero, returns only the nth multipole term;
456
520
  default is 0, which returns the sum of all terms.