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