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 +33 -2
- miepython/core.py +105 -40
- miepython/field.py +238 -293
- 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 +16 -7
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/METADATA +6 -5
- miepython-3.3.0.dist-info/RECORD +18 -0
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/WHEEL +1 -1
- miepython-3.2.0.dist-info/RECORD +0 -18
- {miepython-3.2.0.dist-info → miepython-3.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {miepython-3.2.0.dist-info → miepython-3.3.0.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, 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,137 @@ 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
|
+
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
|
|
258
298
|
a, b, c, d = coefficients(m_rel, x, n_pole=n_pole, internal=True)
|
|
259
299
|
return np.array([a, b, c, d])
|
|
260
300
|
|
|
261
301
|
|
|
262
|
-
def
|
|
263
|
-
"""Evaluate
|
|
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.
|
|
264
304
|
|
|
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.
|
|
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.
|
|
288
310
|
|
|
289
311
|
Args:
|
|
290
|
-
|
|
291
|
-
|
|
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.
|
|
292
317
|
r (float or ndarray): Radial coordinate(s).
|
|
293
318
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
294
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.
|
|
295
323
|
|
|
296
324
|
Returns:
|
|
297
|
-
tuple[ndarray, ndarray]:
|
|
298
|
-
``(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.
|
|
299
327
|
"""
|
|
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.
|
|
328
|
+
a, b, c, d = abcd
|
|
316
329
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
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)
|
|
321
333
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
333
414
|
|
|
334
415
|
|
|
335
416
|
def _spherical_components_to_cartesian(field_sph, r, theta, phi):
|
|
@@ -348,181 +429,6 @@ def _spherical_components_to_cartesian(field_sph, r, theta, phi):
|
|
|
348
429
|
return np.array([fx, fy, fz])
|
|
349
430
|
|
|
350
431
|
|
|
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
432
|
def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=True, n_pole=0, abcd=None):
|
|
527
433
|
"""Calculate the electric field in and around a sphere.
|
|
528
434
|
|
|
@@ -535,7 +441,9 @@ def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
535
441
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
536
442
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
537
443
|
include_incident (bool): Include incident field for points outside sphere.
|
|
538
|
-
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.
|
|
539
447
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
540
448
|
If provided, ``n_pole`` is ignored.
|
|
541
449
|
|
|
@@ -546,10 +454,19 @@ def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
546
454
|
if abcd is None:
|
|
547
455
|
abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
|
|
548
456
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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]
|
|
553
470
|
|
|
554
471
|
|
|
555
472
|
def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=True, n_pole=0, abcd=None):
|
|
@@ -564,7 +481,9 @@ def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
564
481
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
565
482
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
566
483
|
include_incident (bool): Include incident field for points outside sphere.
|
|
567
|
-
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.
|
|
568
487
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
569
488
|
If provided, ``n_pole`` is ignored.
|
|
570
489
|
|
|
@@ -575,10 +494,19 @@ def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=T
|
|
|
575
494
|
if abcd is None:
|
|
576
495
|
abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
|
|
577
496
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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]
|
|
582
510
|
|
|
583
511
|
|
|
584
512
|
def eh_near(
|
|
@@ -604,7 +532,9 @@ def eh_near(
|
|
|
604
532
|
theta (float or ndarray): Polar angle(s) in radians.
|
|
605
533
|
phi (float or ndarray): Azimuth angle(s) in radians.
|
|
606
534
|
include_incident (bool): Include incident field for points outside sphere.
|
|
607
|
-
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.
|
|
608
538
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
609
539
|
If provided, ``n_pole`` is ignored.
|
|
610
540
|
|
|
@@ -615,10 +545,19 @@ def eh_near(
|
|
|
615
545
|
if abcd is None:
|
|
616
546
|
abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
|
|
617
547
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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,
|
|
560
|
+
)
|
|
622
561
|
|
|
623
562
|
|
|
624
563
|
def e_near_cartesian(
|
|
@@ -644,13 +583,15 @@ def e_near_cartesian(
|
|
|
644
583
|
y (float or ndarray): Cartesian y coordinate(s).
|
|
645
584
|
z (float or ndarray): Cartesian z coordinate(s).
|
|
646
585
|
include_incident (bool): Include incident field for points outside sphere.
|
|
647
|
-
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.
|
|
648
589
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
649
590
|
|
|
650
591
|
Returns:
|
|
651
592
|
ndarray: Cartesian electric components ``[E_x, E_y, E_z]``.
|
|
652
593
|
"""
|
|
653
|
-
r, theta, phi =
|
|
594
|
+
r, theta, phi = cartesian_to_spherical(x, y, z)
|
|
654
595
|
e_sph = e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
|
|
655
596
|
return _spherical_components_to_cartesian(e_sph, r, theta, phi)
|
|
656
597
|
|
|
@@ -678,13 +619,15 @@ def h_near_cartesian(
|
|
|
678
619
|
y (float or ndarray): Cartesian y coordinate(s).
|
|
679
620
|
z (float or ndarray): Cartesian z coordinate(s).
|
|
680
621
|
include_incident (bool): Include incident field for points outside sphere.
|
|
681
|
-
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.
|
|
682
625
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
683
626
|
|
|
684
627
|
Returns:
|
|
685
628
|
ndarray: Cartesian magnetic components ``[H_x, H_y, H_z]``.
|
|
686
629
|
"""
|
|
687
|
-
r, theta, phi =
|
|
630
|
+
r, theta, phi = cartesian_to_spherical(x, y, z)
|
|
688
631
|
h_sph = h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
|
|
689
632
|
return _spherical_components_to_cartesian(h_sph, r, theta, phi)
|
|
690
633
|
|
|
@@ -712,14 +655,16 @@ def eh_near_cartesian(
|
|
|
712
655
|
y (float or ndarray): Cartesian y coordinate(s).
|
|
713
656
|
z (float or ndarray): Cartesian z coordinate(s).
|
|
714
657
|
include_incident (bool): Include incident field for points outside sphere.
|
|
715
|
-
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.
|
|
716
661
|
abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
|
|
717
662
|
|
|
718
663
|
Returns:
|
|
719
664
|
tuple[ndarray, ndarray]: Tuple ``(E_xyz, H_xyz)`` where each array is
|
|
720
665
|
``[x, y, z]`` components.
|
|
721
666
|
"""
|
|
722
|
-
r, theta, phi =
|
|
667
|
+
r, theta, phi = cartesian_to_spherical(x, y, z)
|
|
723
668
|
e_sph, h_sph = eh_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
|
|
724
669
|
e_xyz = _spherical_components_to_cartesian(e_sph, r, theta, phi)
|
|
725
670
|
h_xyz = _spherical_components_to_cartesian(h_sph, r, theta, phi)
|