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 +33 -2
- miepython/core.py +172 -40
- miepython/field.py +239 -293
- miepython/mie_jit.py +202 -97
- miepython/mie_nojit.py +166 -86
- miepython/monte_carlo.py +157 -66
- miepython/rayleigh.py +185 -47
- miepython/util.py +25 -5
- miepython/vsh.py +16 -7
- {miepython-3.2.0.dist-info → miepython-3.3.1.dist-info}/METADATA +8 -8
- miepython-3.3.1.dist-info/RECORD +18 -0
- {miepython-3.2.0.dist-info → miepython-3.3.1.dist-info}/WHEEL +1 -1
- miepython-3.2.0.dist-info/RECORD +0 -18
- {miepython-3.2.0.dist-info → miepython-3.3.1.dist-info}/licenses/LICENSE.txt +0 -0
- {miepython-3.2.0.dist-info → miepython-3.3.1.dist-info}/top_level.txt +0 -0
miepython/field.py
CHANGED
|
@@ -81,9 +81,9 @@ Conventions
|
|
|
81
81
|
import numpy as np
|
|
82
82
|
from scipy.special import factorial2, spherical_jn
|
|
83
83
|
from ._backend import D_calc, pi_tau
|
|
84
|
-
from .bessel import
|
|
85
|
-
from .core import S1_S2, coefficients
|
|
86
|
-
from .util import spherical_vector_to_cartesian
|
|
84
|
+
from .bessel import spherical_h1
|
|
85
|
+
from .core import S1_S2, _check_m_x, coefficients, wiscombe_terms
|
|
86
|
+
from .util import cartesian_to_spherical, spherical_vector_to_cartesian
|
|
87
87
|
|
|
88
88
|
__all__ = (
|
|
89
89
|
"e_near",
|
|
@@ -97,62 +97,94 @@ __all__ = (
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def _sum_two_scaled_terms(scale, coeff1, values1, scale1, coeff2, values2, scale2):
|
|
100
|
-
"""
|
|
101
|
-
|
|
100
|
+
"""Sum ``scale * (scale1*coeff1*values1 + scale2*coeff2*values2)`` over multipoles.
|
|
101
|
+
|
|
102
|
+
``values1`` and ``values2`` are shaped ``(n_points, n_terms)`` and the other
|
|
103
|
+
arguments broadcast along the order axis, so the multipole series is summed away
|
|
104
|
+
and one value per point comes back.
|
|
105
|
+
"""
|
|
106
|
+
return np.sum(scale * (scale1 * coeff1 * values1 + scale2 * coeff2 * values2), axis=-1)
|
|
102
107
|
|
|
103
108
|
|
|
104
|
-
def _vsh_components_base(n_terms, lambda0,
|
|
105
|
-
"""Compute shared VSH base components for
|
|
109
|
+
def _vsh_components_base(n_terms, lambda0, m_index, r, theta, inside):
|
|
110
|
+
"""Compute shared VSH base components for a batch of points.
|
|
111
|
+
|
|
112
|
+
Every point in one call must lie on the same side of the sphere surface: a single
|
|
113
|
+
``inside`` flag chooses the radial function and a single ``m_index`` the medium
|
|
114
|
+
for the whole batch. ``_near_fields`` groups the points that way before calling.
|
|
106
115
|
|
|
107
116
|
Args:
|
|
108
117
|
n_terms (int): Number of multipole terms.
|
|
109
118
|
lambda0 (float): Vacuum wavelength.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
m_index (complex): Refractive index at the evaluation points.
|
|
120
|
+
r (ndarray): Radial coordinates, shape ``(n_points,)``.
|
|
121
|
+
theta (ndarray): Polar angles in radians, shape ``(n_points,)``.
|
|
122
|
+
inside (bool): True when the points lie inside the sphere.
|
|
114
123
|
|
|
115
124
|
Returns:
|
|
116
125
|
tuple[ndarray, ndarray, ndarray, ndarray, ndarray]:
|
|
117
|
-
``(M_theta_base, M_phi_base, N_r_base, N_theta_base, N_phi_base)
|
|
118
|
-
for multipole orders ``1..n_terms``.
|
|
126
|
+
``(M_theta_base, M_phi_base, N_r_base, N_theta_base, N_phi_base)``, each
|
|
127
|
+
shaped ``(n_points, n_terms)`` for multipole orders ``1..n_terms``.
|
|
119
128
|
"""
|
|
120
|
-
mu =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
mu = np.cos(theta)
|
|
130
|
+
# the Legendre recurrence is 0/0 exactly at the poles, so nudge mu off them --
|
|
131
|
+
# and only there, leaving every other value untouched
|
|
132
|
+
mu = np.where(mu >= 1.0, 0.999999, mu)
|
|
133
|
+
mu = np.where(mu <= -1.0, -0.999999, mu)
|
|
134
|
+
|
|
135
|
+
n_points = r.size
|
|
136
|
+
pi = np.empty((n_points, n_terms))
|
|
137
|
+
tau = np.empty((n_points, n_terms))
|
|
138
|
+
for k in range(n_points):
|
|
139
|
+
# the kernel writes one point at a time; rows of a C-ordered array are the
|
|
140
|
+
# contiguous 1-D buffers it expects
|
|
141
|
+
pi_tau(float(mu[k]), pi[k], tau[k])
|
|
129
142
|
|
|
130
143
|
n_int = np.arange(1, n_terms + 1, dtype=np.int64)
|
|
131
144
|
n_arr = n_int.astype(np.float64)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
r_col = r[:, np.newaxis]
|
|
146
|
+
rho = 2 * np.pi * m_index * r_col / lambda0
|
|
147
|
+
kr = 2 * np.pi * r_col / lambda0
|
|
135
148
|
|
|
136
149
|
if inside:
|
|
137
150
|
jn = spherical_jn(n_int, rho)
|
|
138
151
|
m_factor = jn
|
|
139
152
|
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
n_factor1 = np.empty((n_points, n_terms), dtype=np.complex128)
|
|
154
|
+
n_factor2 = np.empty((n_points, n_terms), dtype=np.complex128)
|
|
155
|
+
|
|
156
|
+
# near the origin jn/rho and D_n both lose their meaning, so those points
|
|
157
|
+
# take the leading-order series instead; the split is per point
|
|
158
|
+
small = np.abs(rho[:, 0]) < 0.01
|
|
159
|
+
if small.any():
|
|
160
|
+
rho_pow = rho[small] ** np.arange(0, n_terms)
|
|
142
161
|
denom = factorial2(2 * n_int + 1)
|
|
143
|
-
n_factor1 = rho_pow / denom
|
|
144
|
-
n_factor2 = (n_arr + 1.0) * rho_pow / denom
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
n_factor1[small] = rho_pow / denom
|
|
163
|
+
n_factor2[small] = (n_arr + 1.0) * rho_pow / denom
|
|
164
|
+
|
|
165
|
+
large = ~small
|
|
166
|
+
if large.any():
|
|
167
|
+
# D_calc is a scalar kernel, so the logarithmic derivative is still one
|
|
168
|
+
# call per point; only points inside the sphere need it at all
|
|
169
|
+
d_vals = np.empty((int(np.count_nonzero(large)), n_terms), dtype=np.complex128)
|
|
170
|
+
for k, kr_k in enumerate(kr[large, 0]):
|
|
171
|
+
d_vals[k] = D_calc(np.complex128(m_index), float(kr_k), n_terms + 1)[:n_terms]
|
|
172
|
+
n_factor1[large] = jn[large] / rho[large]
|
|
173
|
+
n_factor2[large] = jn[large] * d_vals
|
|
149
174
|
else:
|
|
150
|
-
h1
|
|
175
|
+
# xi'_n needs h1 at orders n-1, n and n+1, so evaluating orders 0..n_terms+1
|
|
176
|
+
# in one call and slicing costs a quarter of what four separate calls do --
|
|
177
|
+
# and h1 at order n is then shared instead of computed twice.
|
|
178
|
+
h_all = spherical_h1(np.arange(0, n_terms + 2, dtype=np.int64), rho)
|
|
179
|
+
h1 = h_all[:, 1 : n_terms + 1]
|
|
151
180
|
m_factor = h1
|
|
152
181
|
n_factor1 = h1 / rho
|
|
153
|
-
|
|
182
|
+
# d_riccati_bessel_h1 written out on the cached values; identical arithmetic,
|
|
183
|
+
# and tests/test_field.py pins it against that function so the two cannot drift
|
|
184
|
+
d_xi = 0.5 * (rho * h_all[:, 0:n_terms] + h1 - rho * h_all[:, 2 : n_terms + 2])
|
|
185
|
+
n_factor2 = d_xi / rho
|
|
154
186
|
|
|
155
|
-
sin_theta = np.sin(theta)
|
|
187
|
+
sin_theta = np.sin(theta)[:, np.newaxis]
|
|
156
188
|
M_theta_base = pi * m_factor
|
|
157
189
|
M_phi_base = tau * m_factor
|
|
158
190
|
N_r_base = n_arr * (n_arr + 1.0) * sin_theta * pi * n_factor1
|
|
@@ -248,88 +280,138 @@ def _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole):
|
|
|
248
280
|
d_sphere (float): Sphere diameter.
|
|
249
281
|
m_sphere (complex): Sphere refractive index.
|
|
250
282
|
n_env (float): Refractive index of the surrounding medium.
|
|
251
|
-
n_pole (int):
|
|
283
|
+
n_pole (int): Number of terms to keep. ``0`` means automatic truncation.
|
|
252
284
|
|
|
253
285
|
Returns:
|
|
254
286
|
ndarray: Coefficients packed as ``[a, b, c, d]``.
|
|
255
287
|
"""
|
|
256
288
|
x = np.pi * d_sphere * n_env / lambda0
|
|
257
289
|
m_rel = m_sphere / n_env
|
|
290
|
+
_check_m_x(m_rel, x) # before wiscombe_terms, which cannot take a NaN x
|
|
291
|
+
if n_pole == 0:
|
|
292
|
+
# Wiscombe's criterion truncates the scattered series, which converges
|
|
293
|
+
# faster than the field evaluated right at the surface. Now that an_bn
|
|
294
|
+
# builds psi_n stably, extra orders converge instead of diverging: the
|
|
295
|
+
# tangential boundary mismatch falls from 1.5e-5 at the criterion to
|
|
296
|
+
# 3.6e-6 with one extra order and 3.2e-6 with two, then flattens out, so
|
|
297
|
+
# two is where it stops being worth more terms.
|
|
298
|
+
n_pole = wiscombe_terms(x) + 2
|
|
258
299
|
a, b, c, d = coefficients(m_rel, x, n_pole=n_pole, internal=True)
|
|
259
300
|
return np.array([a, b, c, d])
|
|
260
301
|
|
|
261
302
|
|
|
262
|
-
def
|
|
263
|
-
"""Evaluate
|
|
303
|
+
def _near_fields(abcd, lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, want_e, want_h):
|
|
304
|
+
"""Evaluate the near fields at every requested point.
|
|
264
305
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
ndarray: Complex array with shape ``(3, ...)``.
|
|
274
|
-
"""
|
|
275
|
-
rr, tt, pp = np.broadcast_arrays(np.asarray(r), np.asarray(theta), np.asarray(phi))
|
|
276
|
-
|
|
277
|
-
if rr.ndim == 0:
|
|
278
|
-
return evaluator(float(rr), float(tt), float(pp))
|
|
279
|
-
|
|
280
|
-
out = np.empty((3,) + rr.shape, dtype=complex)
|
|
281
|
-
for idx in np.ndindex(rr.shape):
|
|
282
|
-
out[(slice(None),) + idx] = evaluator(float(rr[idx]), float(tt[idx]), float(pp[idx]))
|
|
283
|
-
return out
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _vectorized_field_pair_eval(evaluator, r, theta, phi):
|
|
287
|
-
"""Evaluate an ``(E, H)`` callback on scalar or broadcasted inputs.
|
|
306
|
+
The points are split once into those inside the sphere and those outside, and each
|
|
307
|
+
group is evaluated as a batch. That grouping is what makes the work vectorizable:
|
|
308
|
+
the two sides use different radial functions and different media, but within a
|
|
309
|
+
side every point does the same arithmetic, so the multipole series becomes a sum
|
|
310
|
+
over the last axis of a ``(n_points, n_terms)`` array.
|
|
288
311
|
|
|
289
312
|
Args:
|
|
290
|
-
|
|
291
|
-
|
|
313
|
+
abcd (ndarray): Mie coefficients ``[a, b, c, d]``.
|
|
314
|
+
lambda0 (float): Vacuum wavelength.
|
|
315
|
+
d_sphere (float): Sphere diameter.
|
|
316
|
+
m_sphere (complex): Sphere refractive index.
|
|
317
|
+
n_env (float): Refractive index of the surrounding medium.
|
|
292
318
|
r (float or ndarray): Radial coordinate(s).
|
|
293
319
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
294
320
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
321
|
+
include_incident (bool): Include incident field outside the sphere.
|
|
322
|
+
want_e (bool): Compute the electric field.
|
|
323
|
+
want_h (bool): Compute the magnetic field.
|
|
295
324
|
|
|
296
325
|
Returns:
|
|
297
|
-
tuple[ndarray, ndarray]:
|
|
298
|
-
``(3,
|
|
326
|
+
tuple[ndarray or None, ndarray or None]: ``(E, H)`` in spherical components,
|
|
327
|
+
each shaped ``(3,) + broadcast_shape`` or None when not requested.
|
|
299
328
|
"""
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if rr.ndim == 0:
|
|
303
|
-
return evaluator(float(rr), float(tt), float(pp))
|
|
304
|
-
|
|
305
|
-
e_out = np.empty((3,) + rr.shape, dtype=complex)
|
|
306
|
-
h_out = np.empty((3,) + rr.shape, dtype=complex)
|
|
307
|
-
for idx in np.ndindex(rr.shape):
|
|
308
|
-
e_val, h_val = evaluator(float(rr[idx]), float(tt[idx]), float(pp[idx]))
|
|
309
|
-
e_out[(slice(None),) + idx] = e_val
|
|
310
|
-
h_out[(slice(None),) + idx] = h_val
|
|
311
|
-
return e_out, h_out
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def _cartesian_to_spherical_safe(x, y, z):
|
|
315
|
-
"""Convert Cartesian coordinates to spherical coordinates safely.
|
|
329
|
+
a, b, c, d = abcd
|
|
316
330
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
z (float or ndarray): Cartesian z coordinate(s).
|
|
331
|
+
n_terms = len(a)
|
|
332
|
+
nn = np.arange(1, n_terms + 1)
|
|
333
|
+
scale = 1j**nn * (2 * nn + 1) / ((nn + 1) * nn)
|
|
321
334
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
335
|
+
rr, tt, pp = np.broadcast_arrays(
|
|
336
|
+
np.asarray(r, dtype=float),
|
|
337
|
+
np.asarray(theta, dtype=float),
|
|
338
|
+
np.asarray(phi, dtype=float),
|
|
339
|
+
)
|
|
340
|
+
shape = rr.shape
|
|
341
|
+
r_flat = rr.reshape(-1)
|
|
342
|
+
theta_flat = tt.reshape(-1)
|
|
343
|
+
phi_flat = pp.reshape(-1)
|
|
344
|
+
|
|
345
|
+
e_out = np.empty((3, r_flat.size), dtype=complex) if want_e else None
|
|
346
|
+
h_out = np.empty((3, r_flat.size), dtype=complex) if want_h else None
|
|
347
|
+
|
|
348
|
+
is_inside = r_flat < d_sphere / 2
|
|
349
|
+
for inside in (True, False):
|
|
350
|
+
group = is_inside if inside else ~is_inside
|
|
351
|
+
if not group.any():
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# miepython coefficients follow the n-ik convention and are conjugated
|
|
355
|
+
# internally; use conjugated sphere index so internal fields are consistent.
|
|
356
|
+
m_index = np.conjugate(m_sphere) if inside else n_env
|
|
357
|
+
M_the_base, M_phi_base, N_r_base, N_the_base, N_phi_base = _vsh_components_base(
|
|
358
|
+
n_terms, lambda0, m_index, r_flat[group], theta_flat[group], inside
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
cos_phi = np.cos(phi_flat[group])[:, np.newaxis]
|
|
362
|
+
sin_phi = np.sin(phi_flat[group])[:, np.newaxis]
|
|
363
|
+
zero = np.zeros_like(M_the_base, dtype=np.complex128)
|
|
364
|
+
|
|
365
|
+
if want_e:
|
|
366
|
+
# the electric field pairs the odd magnetic modes with the even electric ones
|
|
367
|
+
M_odd_rad, M_odd_the, M_odd_phi = zero, cos_phi * M_the_base, -sin_phi * M_phi_base
|
|
368
|
+
N_even_rad, N_even_the, N_even_phi = (
|
|
369
|
+
cos_phi * N_r_base,
|
|
370
|
+
cos_phi * N_the_base,
|
|
371
|
+
-sin_phi * N_phi_base,
|
|
372
|
+
)
|
|
373
|
+
if inside:
|
|
374
|
+
e_rad = _sum_two_scaled_terms(scale, c, M_odd_rad, 1.0 + 0.0j, d, N_even_rad, -1.0j)
|
|
375
|
+
e_the = _sum_two_scaled_terms(scale, c, M_odd_the, 1.0 + 0.0j, d, N_even_the, -1.0j)
|
|
376
|
+
e_phi = _sum_two_scaled_terms(scale, c, M_odd_phi, 1.0 + 0.0j, d, N_even_phi, -1.0j)
|
|
377
|
+
else:
|
|
378
|
+
e_rad = _sum_two_scaled_terms(scale, a, N_even_rad, 1.0j, b, M_odd_rad, -1.0 + 0.0j)
|
|
379
|
+
e_the = _sum_two_scaled_terms(scale, a, N_even_the, 1.0j, b, M_odd_the, -1.0 + 0.0j)
|
|
380
|
+
e_phi = _sum_two_scaled_terms(scale, a, N_even_phi, 1.0j, b, M_odd_phi, -1.0 + 0.0j)
|
|
381
|
+
if include_incident:
|
|
382
|
+
e_i = _incident_e_spherical(lambda0, n_env, r_flat[group], theta_flat[group], phi_flat[group])
|
|
383
|
+
e_rad += e_i[0]
|
|
384
|
+
e_the += e_i[1]
|
|
385
|
+
e_phi += e_i[2]
|
|
386
|
+
e_out[:, group] = np.array([e_rad, e_the, e_phi])
|
|
387
|
+
|
|
388
|
+
if want_h:
|
|
389
|
+
# and the magnetic field the other way round
|
|
390
|
+
M_even_rad, M_even_the, M_even_phi = zero, -sin_phi * M_the_base, -cos_phi * M_phi_base
|
|
391
|
+
N_odd_rad, N_odd_the, N_odd_phi = (
|
|
392
|
+
sin_phi * N_r_base,
|
|
393
|
+
sin_phi * N_the_base,
|
|
394
|
+
cos_phi * N_phi_base,
|
|
395
|
+
)
|
|
396
|
+
if inside:
|
|
397
|
+
m_rel = np.conjugate(m_sphere / n_env)
|
|
398
|
+
h_rad = m_rel * _sum_two_scaled_terms(scale, d, M_even_rad, -1.0 + 0.0j, c, N_odd_rad, -1.0j)
|
|
399
|
+
h_the = m_rel * _sum_two_scaled_terms(scale, d, M_even_the, -1.0 + 0.0j, c, N_odd_the, -1.0j)
|
|
400
|
+
h_phi = m_rel * _sum_two_scaled_terms(scale, d, M_even_phi, -1.0 + 0.0j, c, N_odd_phi, -1.0j)
|
|
401
|
+
else:
|
|
402
|
+
h_rad = _sum_two_scaled_terms(scale, b, N_odd_rad, 1.0j, a, M_even_rad, 1.0 + 0.0j)
|
|
403
|
+
h_the = _sum_two_scaled_terms(scale, b, N_odd_the, 1.0j, a, M_even_the, 1.0 + 0.0j)
|
|
404
|
+
h_phi = _sum_two_scaled_terms(scale, b, N_odd_phi, 1.0j, a, M_even_phi, 1.0 + 0.0j)
|
|
405
|
+
if include_incident:
|
|
406
|
+
h_i = _incident_h_spherical(lambda0, n_env, r_flat[group], theta_flat[group], phi_flat[group])
|
|
407
|
+
h_rad += h_i[0]
|
|
408
|
+
h_the += h_i[1]
|
|
409
|
+
h_phi += h_i[2]
|
|
410
|
+
h_out[:, group] = np.array([h_rad, h_the, h_phi])
|
|
411
|
+
|
|
412
|
+
e_final = e_out.reshape((3,) + shape) if want_e else None
|
|
413
|
+
h_final = h_out.reshape((3,) + shape) if want_h else None
|
|
414
|
+
return e_final, h_final
|
|
333
415
|
|
|
334
416
|
|
|
335
417
|
def _spherical_components_to_cartesian(field_sph, r, theta, phi):
|
|
@@ -348,181 +430,6 @@ def _spherical_components_to_cartesian(field_sph, r, theta, phi):
|
|
|
348
430
|
return np.array([fx, fy, fz])
|
|
349
431
|
|
|
350
432
|
|
|
351
|
-
def _e_near_abcd(abcd, lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident):
|
|
352
|
-
"""Evaluate near-field electric components with precomputed coefficients.
|
|
353
|
-
|
|
354
|
-
Args:
|
|
355
|
-
abcd (ndarray): Mie coefficients ``[a, b, c, d]``.
|
|
356
|
-
lambda0 (float): Vacuum wavelength.
|
|
357
|
-
d_sphere (float): Sphere diameter.
|
|
358
|
-
m_sphere (complex): Sphere refractive index.
|
|
359
|
-
n_env (float): Refractive index of the surrounding medium.
|
|
360
|
-
r (float): Radial coordinate.
|
|
361
|
-
theta (float): Polar angle in radians.
|
|
362
|
-
phi (float): Azimuth angle in radians.
|
|
363
|
-
include_incident (bool): Include incident field outside the sphere.
|
|
364
|
-
|
|
365
|
-
Returns:
|
|
366
|
-
ndarray: Spherical electric components ``[E_r, E_theta, E_phi]``.
|
|
367
|
-
"""
|
|
368
|
-
a, b, c, d = abcd
|
|
369
|
-
|
|
370
|
-
N = len(a)
|
|
371
|
-
nn = np.arange(1, N + 1)
|
|
372
|
-
scale = 1j**nn * (2 * nn + 1) / ((nn + 1) * nn)
|
|
373
|
-
|
|
374
|
-
inside = r < d_sphere / 2
|
|
375
|
-
# miepython coefficients follow the n-ik convention and are conjugated
|
|
376
|
-
# internally; use conjugated sphere index so internal fields are consistent.
|
|
377
|
-
m_index = np.conjugate(m_sphere) if inside else n_env
|
|
378
|
-
|
|
379
|
-
M_the_base, M_phi_base, N_r_base, N_the_base, N_phi_base = _vsh_components_base(
|
|
380
|
-
N, lambda0, d_sphere, m_index, r, theta
|
|
381
|
-
)
|
|
382
|
-
cos_phi = np.cos(phi)
|
|
383
|
-
sin_phi = np.sin(phi)
|
|
384
|
-
M_rad = np.zeros(N, dtype=np.complex128)
|
|
385
|
-
M_the = cos_phi * M_the_base
|
|
386
|
-
M_phi = -sin_phi * M_phi_base
|
|
387
|
-
N_rad = cos_phi * N_r_base
|
|
388
|
-
N_the = cos_phi * N_the_base
|
|
389
|
-
N_phi = -sin_phi * N_phi_base
|
|
390
|
-
|
|
391
|
-
if inside:
|
|
392
|
-
E_rad = _sum_two_scaled_terms(scale, c, M_rad, 1.0 + 0.0j, d, N_rad, -1.0j)
|
|
393
|
-
E_the = _sum_two_scaled_terms(scale, c, M_the, 1.0 + 0.0j, d, N_the, -1.0j)
|
|
394
|
-
E_phi = _sum_two_scaled_terms(scale, c, M_phi, 1.0 + 0.0j, d, N_phi, -1.0j)
|
|
395
|
-
else:
|
|
396
|
-
E_rad = _sum_two_scaled_terms(scale, a, N_rad, 1.0j, b, M_rad, -1.0 + 0.0j)
|
|
397
|
-
E_the = _sum_two_scaled_terms(scale, a, N_the, 1.0j, b, M_the, -1.0 + 0.0j)
|
|
398
|
-
E_phi = _sum_two_scaled_terms(scale, a, N_phi, 1.0j, b, M_phi, -1.0 + 0.0j)
|
|
399
|
-
|
|
400
|
-
if include_incident:
|
|
401
|
-
Ei_rad, Ei_the, Ei_phi = _incident_e_spherical(lambda0, n_env, r, theta, phi)
|
|
402
|
-
E_rad += Ei_rad
|
|
403
|
-
E_the += Ei_the
|
|
404
|
-
E_phi += Ei_phi
|
|
405
|
-
|
|
406
|
-
return np.array([E_rad, E_the, E_phi])
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def _h_near_abcd(abcd, lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident):
|
|
410
|
-
"""Evaluate near-field magnetic components with precomputed coefficients.
|
|
411
|
-
|
|
412
|
-
Args:
|
|
413
|
-
abcd (ndarray): Mie coefficients ``[a, b, c, d]``.
|
|
414
|
-
lambda0 (float): Vacuum wavelength.
|
|
415
|
-
d_sphere (float): Sphere diameter.
|
|
416
|
-
m_sphere (complex): Sphere refractive index.
|
|
417
|
-
n_env (float): Refractive index of the surrounding medium.
|
|
418
|
-
r (float): Radial coordinate.
|
|
419
|
-
theta (float): Polar angle in radians.
|
|
420
|
-
phi (float): Azimuth angle in radians.
|
|
421
|
-
include_incident (bool): Include incident field outside the sphere.
|
|
422
|
-
|
|
423
|
-
Returns:
|
|
424
|
-
ndarray: Spherical magnetic components ``[H_r, H_theta, H_phi]``.
|
|
425
|
-
"""
|
|
426
|
-
a, b, c, d = abcd
|
|
427
|
-
|
|
428
|
-
N = len(a)
|
|
429
|
-
nn = np.arange(1, N + 1)
|
|
430
|
-
scale = 1j**nn * (2 * nn + 1) / ((nn + 1) * nn)
|
|
431
|
-
|
|
432
|
-
inside = r < d_sphere / 2
|
|
433
|
-
m_index = np.conjugate(m_sphere) if inside else n_env
|
|
434
|
-
|
|
435
|
-
M_the_base, M_phi_base, N_r_base, N_the_base, N_phi_base = _vsh_components_base(
|
|
436
|
-
N, lambda0, d_sphere, m_index, r, theta
|
|
437
|
-
)
|
|
438
|
-
cos_phi = np.cos(phi)
|
|
439
|
-
sin_phi = np.sin(phi)
|
|
440
|
-
M_rad = np.zeros(N, dtype=np.complex128)
|
|
441
|
-
M_the = -sin_phi * M_the_base
|
|
442
|
-
M_phi = -cos_phi * M_phi_base
|
|
443
|
-
N_rad = sin_phi * N_r_base
|
|
444
|
-
N_the = sin_phi * N_the_base
|
|
445
|
-
N_phi = cos_phi * N_phi_base
|
|
446
|
-
|
|
447
|
-
if inside:
|
|
448
|
-
m_rel = np.conjugate(m_sphere / n_env)
|
|
449
|
-
H_rad = m_rel * _sum_two_scaled_terms(scale, d, M_rad, -1.0 + 0.0j, c, N_rad, -1.0j)
|
|
450
|
-
H_the = m_rel * _sum_two_scaled_terms(scale, d, M_the, -1.0 + 0.0j, c, N_the, -1.0j)
|
|
451
|
-
H_phi = m_rel * _sum_two_scaled_terms(scale, d, M_phi, -1.0 + 0.0j, c, N_phi, -1.0j)
|
|
452
|
-
else:
|
|
453
|
-
H_rad = _sum_two_scaled_terms(scale, b, N_rad, 1.0j, a, M_rad, 1.0 + 0.0j)
|
|
454
|
-
H_the = _sum_two_scaled_terms(scale, b, N_the, 1.0j, a, M_the, 1.0 + 0.0j)
|
|
455
|
-
H_phi = _sum_two_scaled_terms(scale, b, N_phi, 1.0j, a, M_phi, 1.0 + 0.0j)
|
|
456
|
-
|
|
457
|
-
if include_incident:
|
|
458
|
-
Hi_rad, Hi_the, Hi_phi = _incident_h_spherical(lambda0, n_env, r, theta, phi)
|
|
459
|
-
H_rad += Hi_rad
|
|
460
|
-
H_the += Hi_the
|
|
461
|
-
H_phi += Hi_phi
|
|
462
|
-
|
|
463
|
-
return np.array([H_rad, H_the, H_phi])
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
def _eh_near_abcd(abcd, lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident):
|
|
467
|
-
"""Evaluate electric and magnetic near fields with shared VSH work."""
|
|
468
|
-
a, b, c, d = abcd
|
|
469
|
-
|
|
470
|
-
N = len(a)
|
|
471
|
-
nn = np.arange(1, N + 1)
|
|
472
|
-
scale = 1j**nn * (2 * nn + 1) / ((nn + 1) * nn)
|
|
473
|
-
|
|
474
|
-
inside = r < d_sphere / 2
|
|
475
|
-
m_index = np.conjugate(m_sphere) if inside else n_env
|
|
476
|
-
M_the_base, M_phi_base, N_r_base, N_the_base, N_phi_base = _vsh_components_base(
|
|
477
|
-
N, lambda0, d_sphere, m_index, r, theta
|
|
478
|
-
)
|
|
479
|
-
cos_phi = np.cos(phi)
|
|
480
|
-
sin_phi = np.sin(phi)
|
|
481
|
-
|
|
482
|
-
M_odd_rad = np.zeros(N, dtype=np.complex128)
|
|
483
|
-
M_odd_the = cos_phi * M_the_base
|
|
484
|
-
M_odd_phi = -sin_phi * M_phi_base
|
|
485
|
-
N_even_rad = cos_phi * N_r_base
|
|
486
|
-
N_even_the = cos_phi * N_the_base
|
|
487
|
-
N_even_phi = -sin_phi * N_phi_base
|
|
488
|
-
|
|
489
|
-
M_even_rad = np.zeros(N, dtype=np.complex128)
|
|
490
|
-
M_even_the = -sin_phi * M_the_base
|
|
491
|
-
M_even_phi = -cos_phi * M_phi_base
|
|
492
|
-
N_odd_rad = sin_phi * N_r_base
|
|
493
|
-
N_odd_the = sin_phi * N_the_base
|
|
494
|
-
N_odd_phi = cos_phi * N_phi_base
|
|
495
|
-
|
|
496
|
-
if inside:
|
|
497
|
-
e_rad = _sum_two_scaled_terms(scale, c, M_odd_rad, 1.0 + 0.0j, d, N_even_rad, -1.0j)
|
|
498
|
-
e_the = _sum_two_scaled_terms(scale, c, M_odd_the, 1.0 + 0.0j, d, N_even_the, -1.0j)
|
|
499
|
-
e_phi = _sum_two_scaled_terms(scale, c, M_odd_phi, 1.0 + 0.0j, d, N_even_phi, -1.0j)
|
|
500
|
-
|
|
501
|
-
m_rel = np.conjugate(m_sphere / n_env)
|
|
502
|
-
h_rad = m_rel * _sum_two_scaled_terms(scale, d, M_even_rad, -1.0 + 0.0j, c, N_odd_rad, -1.0j)
|
|
503
|
-
h_the = m_rel * _sum_two_scaled_terms(scale, d, M_even_the, -1.0 + 0.0j, c, N_odd_the, -1.0j)
|
|
504
|
-
h_phi = m_rel * _sum_two_scaled_terms(scale, d, M_even_phi, -1.0 + 0.0j, c, N_odd_phi, -1.0j)
|
|
505
|
-
else:
|
|
506
|
-
e_rad = _sum_two_scaled_terms(scale, a, N_even_rad, 1.0j, b, M_odd_rad, -1.0 + 0.0j)
|
|
507
|
-
e_the = _sum_two_scaled_terms(scale, a, N_even_the, 1.0j, b, M_odd_the, -1.0 + 0.0j)
|
|
508
|
-
e_phi = _sum_two_scaled_terms(scale, a, N_even_phi, 1.0j, b, M_odd_phi, -1.0 + 0.0j)
|
|
509
|
-
h_rad = _sum_two_scaled_terms(scale, b, N_odd_rad, 1.0j, a, M_even_rad, 1.0 + 0.0j)
|
|
510
|
-
h_the = _sum_two_scaled_terms(scale, b, N_odd_the, 1.0j, a, M_even_the, 1.0 + 0.0j)
|
|
511
|
-
h_phi = _sum_two_scaled_terms(scale, b, N_odd_phi, 1.0j, a, M_even_phi, 1.0 + 0.0j)
|
|
512
|
-
|
|
513
|
-
if include_incident:
|
|
514
|
-
e_i = _incident_e_spherical(lambda0, n_env, r, theta, phi)
|
|
515
|
-
h_i = _incident_h_spherical(lambda0, n_env, r, theta, phi)
|
|
516
|
-
e_rad += e_i[0]
|
|
517
|
-
e_the += e_i[1]
|
|
518
|
-
e_phi += e_i[2]
|
|
519
|
-
h_rad += h_i[0]
|
|
520
|
-
h_the += h_i[1]
|
|
521
|
-
h_phi += h_i[2]
|
|
522
|
-
|
|
523
|
-
return np.array([e_rad, e_the, e_phi]), np.array([h_rad, h_the, h_phi])
|
|
524
|
-
|
|
525
|
-
|
|
526
433
|
def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=True, n_pole=0, abcd=None):
|
|
527
434
|
"""Calculate the electric field in and around a sphere.
|
|
528
435
|
|
|
@@ -535,7 +442,9 @@ def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
535
442
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
536
443
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
537
444
|
include_incident (bool): Include incident field for points outside sphere.
|
|
538
|
-
n_pole (int):
|
|
445
|
+
n_pole (int): Number of multipole terms to keep. ``0`` (the default) keeps
|
|
446
|
+
two more than Wiscombe's criterion, which converges the near field at
|
|
447
|
+
the sphere surface roughly five times closer than the criterion alone.
|
|
539
448
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
540
449
|
If provided, ``n_pole`` is ignored.
|
|
541
450
|
|
|
@@ -546,10 +455,19 @@ def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
546
455
|
if abcd is None:
|
|
547
456
|
abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
|
|
548
457
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
458
|
+
return _near_fields(
|
|
459
|
+
abcd,
|
|
460
|
+
lambda0,
|
|
461
|
+
d_sphere,
|
|
462
|
+
m_sphere,
|
|
463
|
+
n_env,
|
|
464
|
+
r,
|
|
465
|
+
theta,
|
|
466
|
+
phi,
|
|
467
|
+
include_incident,
|
|
468
|
+
want_e=True,
|
|
469
|
+
want_h=False,
|
|
470
|
+
)[0]
|
|
553
471
|
|
|
554
472
|
|
|
555
473
|
def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=True, n_pole=0, abcd=None):
|
|
@@ -564,7 +482,9 @@ def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
564
482
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
565
483
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
566
484
|
include_incident (bool): Include incident field for points outside sphere.
|
|
567
|
-
n_pole (int):
|
|
485
|
+
n_pole (int): Number of multipole terms to keep. ``0`` (the default) keeps
|
|
486
|
+
two more than Wiscombe's criterion, which converges the near field at
|
|
487
|
+
the sphere surface roughly five times closer than the criterion alone.
|
|
568
488
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
569
489
|
If provided, ``n_pole`` is ignored.
|
|
570
490
|
|
|
@@ -575,10 +495,19 @@ def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
575
495
|
if abcd is None:
|
|
576
496
|
abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
|
|
577
497
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
498
|
+
return _near_fields(
|
|
499
|
+
abcd,
|
|
500
|
+
lambda0,
|
|
501
|
+
d_sphere,
|
|
502
|
+
m_sphere,
|
|
503
|
+
n_env,
|
|
504
|
+
r,
|
|
505
|
+
theta,
|
|
506
|
+
phi,
|
|
507
|
+
include_incident,
|
|
508
|
+
want_e=False,
|
|
509
|
+
want_h=True,
|
|
510
|
+
)[1]
|
|
582
511
|
|
|
583
512
|
|
|
584
513
|
def eh_near(
|
|
@@ -604,7 +533,9 @@ def eh_near(
|
|
|
604
533
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
605
534
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
606
535
|
include_incident (bool): Include incident field for points outside sphere.
|
|
607
|
-
n_pole (int):
|
|
536
|
+
n_pole (int): Number of multipole terms to keep. ``0`` (the default) keeps
|
|
537
|
+
two more than Wiscombe's criterion, which converges the near field at
|
|
538
|
+
the sphere surface roughly five times closer than the criterion alone.
|
|
608
539
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
609
540
|
If provided, ``n_pole`` is ignored.
|
|
610
541
|
|
|
@@ -615,10 +546,19 @@ def eh_near(
|
|
|
615
546
|
if abcd is None:
|
|
616
547
|
abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
|
|
617
548
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
549
|
+
return _near_fields(
|
|
550
|
+
abcd,
|
|
551
|
+
lambda0,
|
|
552
|
+
d_sphere,
|
|
553
|
+
m_sphere,
|
|
554
|
+
n_env,
|
|
555
|
+
r,
|
|
556
|
+
theta,
|
|
557
|
+
phi,
|
|
558
|
+
include_incident,
|
|
559
|
+
want_e=True,
|
|
560
|
+
want_h=True,
|
|
561
|
+
)
|
|
622
562
|
|
|
623
563
|
|
|
624
564
|
def e_near_cartesian(
|
|
@@ -644,13 +584,15 @@ def e_near_cartesian(
|
|
|
644
584
|
y (float or ndarray): Cartesian y coordinate(s).
|
|
645
585
|
z (float or ndarray): Cartesian z coordinate(s).
|
|
646
586
|
include_incident (bool): Include incident field for points outside sphere.
|
|
647
|
-
n_pole (int):
|
|
587
|
+
n_pole (int): Number of multipole terms to keep. ``0`` (the default) keeps
|
|
588
|
+
two more than Wiscombe's criterion, which converges the near field at
|
|
589
|
+
the sphere surface roughly five times closer than the criterion alone.
|
|
648
590
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
649
591
|
|
|
650
592
|
Returns:
|
|
651
593
|
ndarray: Cartesian electric components ``[E_x, E_y, E_z]``.
|
|
652
594
|
"""
|
|
653
|
-
r, theta, phi =
|
|
595
|
+
r, theta, phi = cartesian_to_spherical(x, y, z)
|
|
654
596
|
e_sph = e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
|
|
655
597
|
return _spherical_components_to_cartesian(e_sph, r, theta, phi)
|
|
656
598
|
|
|
@@ -678,13 +620,15 @@ def h_near_cartesian(
|
|
|
678
620
|
y (float or ndarray): Cartesian y coordinate(s).
|
|
679
621
|
z (float or ndarray): Cartesian z coordinate(s).
|
|
680
622
|
include_incident (bool): Include incident field for points outside sphere.
|
|
681
|
-
n_pole (int):
|
|
623
|
+
n_pole (int): Number of multipole terms to keep. ``0`` (the default) keeps
|
|
624
|
+
two more than Wiscombe's criterion, which converges the near field at
|
|
625
|
+
the sphere surface roughly five times closer than the criterion alone.
|
|
682
626
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
683
627
|
|
|
684
628
|
Returns:
|
|
685
629
|
ndarray: Cartesian magnetic components ``[H_x, H_y, H_z]``.
|
|
686
630
|
"""
|
|
687
|
-
r, theta, phi =
|
|
631
|
+
r, theta, phi = cartesian_to_spherical(x, y, z)
|
|
688
632
|
h_sph = h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
|
|
689
633
|
return _spherical_components_to_cartesian(h_sph, r, theta, phi)
|
|
690
634
|
|
|
@@ -712,14 +656,16 @@ def eh_near_cartesian(
|
|
|
712
656
|
y (float or ndarray): Cartesian y coordinate(s).
|
|
713
657
|
z (float or ndarray): Cartesian z coordinate(s).
|
|
714
658
|
include_incident (bool): Include incident field for points outside sphere.
|
|
715
|
-
n_pole (int):
|
|
659
|
+
n_pole (int): Number of multipole terms to keep. ``0`` (the default) keeps
|
|
660
|
+
two more than Wiscombe's criterion, which converges the near field at
|
|
661
|
+
the sphere surface roughly five times closer than the criterion alone.
|
|
716
662
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
717
663
|
|
|
718
664
|
Returns:
|
|
719
665
|
tuple[ndarray, ndarray]: Tuple ``(E_xyz, H_xyz)`` where each array is
|
|
720
666
|
``[x, y, z]`` components.
|
|
721
667
|
"""
|
|
722
|
-
r, theta, phi =
|
|
668
|
+
r, theta, phi = cartesian_to_spherical(x, y, z)
|
|
723
669
|
e_sph, h_sph = eh_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
|
|
724
670
|
e_xyz = _spherical_components_to_cartesian(e_sph, r, theta, phi)
|
|
725
671
|
h_xyz = _spherical_components_to_cartesian(h_sph, r, theta, phi)
|