miepython 3.0.5__py3-none-any.whl → 3.2.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/field.py CHANGED
@@ -1,38 +1,188 @@
1
- """
2
- Electric and magnetic field calculations.
1
+ """Near- and far-field electromagnetic fields for a homogeneous sphere.
2
+
3
+ This module evaluates complex phasor electric and magnetic fields for a sphere
4
+ embedded in a uniform medium.
5
+
6
+ Main entry points
7
+ -----------------
8
+ Use these for most workflows:
9
+
10
+ - ``e_near(...)``: electric field in spherical components ``[E_r, E_theta, E_phi]``.
11
+ - ``h_near(...)``: magnetic field in spherical components ``[H_r, H_theta, H_phi]``.
12
+ - ``eh_near(...)``: both electric and magnetic fields in one call.
13
+ - ``e_near_cartesian(...)`` / ``h_near_cartesian(...)`` / ``eh_near_cartesian(...)``:
14
+ same near-field calculations in Cartesian components ``[Fx, Fy, Fz]``.
15
+ - ``e_far(...)``: scattered far-field electric components.
16
+
17
+ Quick examples
18
+ --------------
19
+ Evaluate the near electric field on a ring of points:
20
+
21
+ >>> import numpy as np
22
+ >>> import miepython.field as fields
23
+ >>> theta = np.linspace(0.0, np.pi, 181)
24
+ >>> phi = np.zeros_like(theta)
25
+ >>> r = np.full_like(theta, 1.5) # same length units as lambda0 and d_sphere
26
+ >>> E = fields.e_near(
27
+ ... lambda0=1.0,
28
+ ... d_sphere=1.0,
29
+ ... m_sphere=1.5 + 0.0j,
30
+ ... n_env=1.0,
31
+ ... r=r,
32
+ ... theta=theta,
33
+ ... phi=phi,
34
+ ... )
35
+ >>> E.shape
36
+ (3, 181)
37
+
38
+ Evaluate both fields in Cartesian coordinates on a 2D x-z slice:
39
+
40
+ >>> u = np.linspace(-1.5, 1.5, 101)
41
+ >>> X, Z = np.meshgrid(u, u, indexing="xy")
42
+ >>> E_xyz, H_xyz = fields.eh_near_cartesian(
43
+ ... lambda0=1.0,
44
+ ... d_sphere=1.0,
45
+ ... m_sphere=1.5 + 0.0j,
46
+ ... n_env=1.0,
47
+ ... x=X,
48
+ ... y=np.zeros_like(X),
49
+ ... z=Z,
50
+ ... )
51
+ >>> E_xyz.shape, H_xyz.shape
52
+ ((3, 101, 101), (3, 101, 101))
53
+
54
+ Reuse precomputed Mie coefficients for repeated field evaluations:
55
+
56
+ >>> import miepython as mie
57
+ >>> m_rel = (1.5 + 0.0j) / 1.0
58
+ >>> x_size = np.pi * 1.0 * 1.0 / 1.0
59
+ >>> a, b, c, d = mie.coefficients(m_rel, x_size, internal=True)
60
+ >>> abcd = np.array([a, b, c, d])
61
+ >>> E2, H2 = fields.eh_near(
62
+ ... 1.0, 1.0, 1.5 + 0.0j, 1.0, r, theta, phi, abcd=abcd
63
+ ... )
3
64
 
4
- Only e_far has been verified to work.
65
+ Conventions
66
+ -----------
67
+ - Phasor time dependence: ``exp(-i * omega * t)``.
68
+ - Incident plane wave: propagation along ``+z``, ``E`` along ``+x``, ``H`` along ``+y``,
69
+ incident amplitude ``E0 = 1``.
70
+ - Spherical coordinates: ``theta`` from ``+z`` and ``phi`` from ``+x`` toward ``+y``.
71
+ - ``lambda0`` is vacuum wavelength.
72
+ - ``n_env`` is the surrounding-medium index and ``m_sphere`` is the sphere index;
73
+ relative index is ``m_rel = m_sphere / n_env``.
74
+ - Size parameter is ``x = pi * d_sphere * n_env / lambda0``.
75
+ - Near field definition:
76
+ outside the sphere, returned field is incident plus scattered;
77
+ inside the sphere, returned field is the internal field.
78
+ - Non-magnetic materials are assumed (relative permeability ``mu_r = 1``).
5
79
  """
6
80
 
7
81
  import numpy as np
8
- from miepython.vsh import M_odd_array, N_even_array
9
- from miepython.util import cartesian_to_spherical, spherical_vector_to_cartesian
10
- import miepython as mie
82
+ from scipy.special import factorial2, spherical_jn
83
+ from ._backend import D_calc, pi_tau
84
+ from .bessel import d_riccati_bessel_h1, spherical_h1
85
+ from .core import S1_S2, coefficients
86
+ from .util import spherical_vector_to_cartesian
11
87
 
12
- __all__ = ("e_near", "e_plane", "e_far")
88
+ __all__ = (
89
+ "e_near",
90
+ "h_near",
91
+ "eh_near",
92
+ "e_near_cartesian",
93
+ "h_near_cartesian",
94
+ "eh_near_cartesian",
95
+ "e_far",
96
+ )
13
97
 
14
98
 
15
- def e_far(lambda0, d_sphere, m_sphere, r, theta, phi):
99
+ def _sum_two_scaled_terms(scale, coeff1, values1, scale1, coeff2, values2, scale2):
100
+ """Return ``sum(scale * (scale1*coeff1*values1 + scale2*coeff2*values2))``."""
101
+ return np.sum(scale * (scale1 * coeff1 * values1 + scale2 * coeff2 * values2))
102
+
103
+
104
+ def _vsh_components_base(n_terms, lambda0, d_sphere, m_index, r, theta):
105
+ """Compute shared VSH base components for one spatial point.
106
+
107
+ Args:
108
+ n_terms (int): Number of multipole terms.
109
+ lambda0 (float): Vacuum wavelength.
110
+ d_sphere (float): Sphere diameter.
111
+ m_index (complex): Refractive index at the evaluation point.
112
+ r (float): Radial coordinate.
113
+ theta (float): Polar angle in radians.
114
+
115
+ Returns:
116
+ 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``.
16
119
  """
17
- Calculate the electric field in the far field.
120
+ mu = float(np.cos(theta))
121
+ if mu >= 1.0:
122
+ mu = 0.999999
123
+ elif mu <= -1.0:
124
+ mu = -0.999999
125
+
126
+ pi = np.empty(n_terms)
127
+ tau = np.empty(n_terms)
128
+ pi_tau(mu, pi, tau)
129
+
130
+ n_int = np.arange(1, n_terms + 1, dtype=np.int64)
131
+ n_arr = n_int.astype(np.float64)
132
+ rho = 2 * np.pi * m_index * r / lambda0
133
+ kr = 2 * np.pi * r / lambda0
134
+ inside = r < d_sphere / 2
135
+
136
+ if inside:
137
+ jn = spherical_jn(n_int, rho)
138
+ m_factor = jn
139
+
140
+ if np.abs(rho) < 0.01:
141
+ rho_pow = rho ** np.arange(0, n_terms)
142
+ denom = factorial2(2 * n_int + 1)
143
+ n_factor1 = rho_pow / denom
144
+ n_factor2 = (n_arr + 1.0) * rho_pow / denom
145
+ else:
146
+ d_vals = D_calc(np.complex128(m_index), float(kr), n_terms + 1)[:n_terms]
147
+ n_factor1 = jn / rho
148
+ n_factor2 = jn * d_vals
149
+ else:
150
+ h1 = spherical_h1(n_int, rho)
151
+ m_factor = h1
152
+ n_factor1 = h1 / rho
153
+ n_factor2 = d_riccati_bessel_h1(n_int, rho) / rho
154
+
155
+ sin_theta = np.sin(theta)
156
+ M_theta_base = pi * m_factor
157
+ M_phi_base = tau * m_factor
158
+ N_r_base = n_arr * (n_arr + 1.0) * sin_theta * pi * n_factor1
159
+ N_theta_base = tau * n_factor2
160
+ N_phi_base = pi * n_factor2
161
+ return M_theta_base, M_phi_base, N_r_base, N_theta_base, N_phi_base
162
+
163
+
164
+ def e_far(lambda0, d_sphere, m_sphere, n_env, r, theta, phi):
165
+ """Evaluate the scattered electric far field.
18
166
 
19
167
  Args:
20
- lambda0 (float): Wavelength the incident wave in vacuum.
21
- d_sphere (float): Diameter of the sphere.
22
- m_sphere (complex): Rrefractive index of the sphere
23
- r (float): Radial distance at which the field is evaluated.
24
- theta (float): Scattering angle (from z-axis) in radians.
25
- phi (float): Azimuthal angle (from x-axis) in radians.
26
- norm: type of normalization to use for scattering function
168
+ lambda0 (float): Vacuum wavelength.
169
+ d_sphere (float): Sphere diameter.
170
+ m_sphere (complex): Sphere refractive index.
171
+ n_env (float): Refractive index of the surrounding medium.
172
+ r (float or ndarray): Radial observation distance.
173
+ theta (float or ndarray): Polar angle in radians.
174
+ phi (float or ndarray): Azimuth angle in radians.
27
175
 
28
176
  Returns:
29
- tuple: Electric field components (E_r, E_theta, E_phi).
177
+ ndarray: Complex spherical components ``[E_r, E_theta, E_phi]`` with
178
+ shape ``(3, ...)`` following broadcasted input shape.
30
179
  """
31
- x = np.pi * d_sphere / lambda0
32
- jkr = 1j * 2 * np.pi * r / lambda0
180
+ x = np.pi * d_sphere * n_env / lambda0
181
+ m_rel = m_sphere / n_env
182
+ jkr = 1j * 2 * np.pi * n_env * r / lambda0
33
183
  amp = np.exp(jkr) / (-jkr)
34
184
 
35
- S1, S2 = mie.S1_S2(m_sphere, x, np.cos(theta), norm="wiscombe")
185
+ S1, S2 = S1_S2(m_rel, x, np.cos(theta), norm="wiscombe")
36
186
 
37
187
  E_r = np.zeros_like(S1, dtype=complex)
38
188
  E_theta = S2 * amp * np.cos(phi)
@@ -42,102 +192,178 @@ def e_far(lambda0, d_sphere, m_sphere, r, theta, phi):
42
192
  return np.array([E_r, E_theta, E_phi])
43
193
 
44
194
 
45
- # def e_far_old(lambda0, d_sphere, m_sphere, r, theta, phi):
46
- # """
47
- # Calculate the electric field in the far field.
48
- #
49
- # Args:
50
- # lambda0 (float): Wavelength the incident wave in vacuum.
51
- # d_sphere (float): Diameter of the sphere.
52
- # m_sphere (complex): Rrefractive index of the sphere
53
- # r (float): Radial distance at which the field is evaluated.
54
- # theta (float): Scattering angle (from z-axis) in radians.
55
- # phi (float): Azimuthal angle (from x-axis) in radians.
56
- # norm: type of normalization to use for scattering function
57
- #
58
- # Returns:
59
- # tuple: Electric field components (E_r, E_theta, E_phi).
60
- # """
61
- # x = np.pi * d_sphere / lambda0
62
- # jkr = 1j * 2 * np.pi * r / lambda0
63
- # amp = np.exp(jkr) / (-jkr)
64
- # mu = np.cos(theta)
65
- #
66
- # a, b = mie.an_bn(m_sphere, x, 0)
67
- # N = len(a)
68
- # pi = np.zeros(N)
69
- # tau = np.zeros(N)
70
- #
71
- # n = np.arange(1, N + 1)
72
- # scale = (2 * n + 1) / ((n + 1) * n)
73
- #
74
- # mie._pi_tau(mu, pi, tau)
75
- #
76
- # E_r = complex(0)
77
- # E_theta = np.sum(scale * (tau * a + pi * b)) * amp * np.cos(phi)
78
- # E_phi = np.sum(scale * (pi * a + tau * b)) * amp * np.sin(phi)
79
- # return np.array([E_r, E_theta, E_phi])
80
- #
81
- #
82
- # def e_near_old(abcd, lambda0, d_sphere, m_index, r, theta, phi):
83
- # """
84
- # Calculate the electric field in and around a sphere.
85
- #
86
- # Args:
87
- # abcd (array): Mie coefficients [a, b, c, d]
88
- # lambda0 (float): Wavelength of the incident wave in vacuum.
89
- # d_sphere (float): Diameter of the sphere.
90
- # m_index (complex): refractive index at r
91
- # r (float): Radial distance at which the field is evaluated.
92
- # theta (float): Polar angle in radians.
93
- # phi (float): Azimuthal angle in radians.
94
- #
95
- # Returns:
96
- # tuple: Electric field components (E_r, E_theta, E_phi).
97
- # """
98
- # a, b, c, d = abcd
99
- # E_r = np.complex128(0)
100
- # E_theta = np.complex128(0)
101
- # E_phi = np.complex128(0)
102
- #
103
- # N = len(a)
104
- # nn = np.arange(1, N + 1)
105
- # scale = 1j**nn * (2 * nn + 1) / ((nn + 1) * nn)
106
- #
107
- # inside = r < d_sphere / 2
108
- #
109
- # for n in nn:
110
- # Mn = M_odd(n, lambda0, d_sphere, m_index, r, theta, phi)
111
- # Nn = N_even(n, lambda0, d_sphere, m_index, r, theta, phi)
112
- #
113
- # if inside:
114
- # E_r += scale[n - 1] * (c[n - 1] * Mn[0] - 1j * d[n - 1] * Nn[0])
115
- # E_theta += scale[n - 1] * (c[n - 1] * Mn[1] - 1j * d[n - 1] * Nn[1])
116
- # E_phi += scale[n - 1] * (c[n - 1] * Mn[2] - 1j * d[n - 1] * Nn[2])
117
- # else:
118
- # E_r += scale[n - 1] * (1j * a[n - 1] * Nn[0] - b[n - 1] * Mn[0])
119
- # th_part = scale[n - 1] * (1j * a[n - 1] * Nn[1] - b[n - 1] * Mn[1])
120
- # E_theta += th_part
121
- # E_phi += -scale[n - 1] * (1j * a[n - 1] * Nn[2] - b[n - 1] * Mn[2])
122
- #
123
- # return np.array([E_r, E_theta, E_phi])
124
-
125
-
126
- def e_near(abcd, lambda0, d_sphere, m_index, r, theta, phi):
127
- """
128
- Calculate the electric field in and around a sphere.
129
-
130
- Args:
131
- abcd (array): Mie coefficients [a, b, c, d]
132
- lambda0 (float): Wavelength of the incident wave in vacuum.
133
- d_sphere (float): Diameter of the sphere.
134
- m_index (complex): refractive index at r
135
- r (float): Radial distance at which the field is evaluated.
195
+ def _incident_e_spherical(lambda0, n_env, r, theta, phi, amplitude=1.0):
196
+ """Return incident electric plane-wave components in spherical coordinates.
197
+
198
+ Args:
199
+ lambda0 (float): Vacuum wavelength.
200
+ n_env (float): Refractive index of the surrounding medium.
201
+ r (float): Radial coordinate.
136
202
  theta (float): Polar angle in radians.
137
- phi (float): Azimuthal angle in radians.
203
+ phi (float): Azimuth angle in radians.
204
+ amplitude (complex): Incident electric field amplitude.
205
+
206
+ Returns:
207
+ ndarray: Complex spherical vector ``[E_r, E_theta, E_phi]``.
208
+ """
209
+ k = 2 * np.pi * n_env / lambda0
210
+ phase = np.exp(1j * k * r * np.cos(theta))
211
+ amp = amplitude * phase
212
+
213
+ e_r = amp * np.sin(theta) * np.cos(phi)
214
+ e_theta = amp * np.cos(theta) * np.cos(phi)
215
+ e_phi = -amp * np.sin(phi)
216
+ return np.array([e_r, e_theta, e_phi])
217
+
218
+
219
+ def _incident_h_spherical(lambda0, n_env, r, theta, phi, amplitude=1.0):
220
+ """Return incident magnetic plane-wave components in spherical coordinates.
221
+
222
+ Args:
223
+ lambda0 (float): Vacuum wavelength.
224
+ n_env (float): Refractive index of the surrounding medium.
225
+ r (float): Radial coordinate.
226
+ theta (float): Polar angle in radians.
227
+ phi (float): Azimuth angle in radians.
228
+ amplitude (complex): Incident magnetic field amplitude in normalized units.
229
+
230
+ Returns:
231
+ ndarray: Complex spherical vector ``[H_r, H_theta, H_phi]``.
232
+ """
233
+ k = 2 * np.pi * n_env / lambda0
234
+ phase = np.exp(1j * k * r * np.cos(theta))
235
+ amp = amplitude * phase
236
+
237
+ h_r = amp * np.sin(theta) * np.sin(phi)
238
+ h_theta = amp * np.cos(theta) * np.sin(phi)
239
+ h_phi = amp * np.cos(phi)
240
+ return np.array([h_r, h_theta, h_phi])
241
+
242
+
243
+ def _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole):
244
+ """Compute Mie coefficients with consistent medium scaling.
245
+
246
+ Args:
247
+ lambda0 (float): Vacuum wavelength.
248
+ d_sphere (float): Sphere diameter.
249
+ m_sphere (complex): Sphere refractive index.
250
+ n_env (float): Refractive index of the surrounding medium.
251
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
252
+
253
+ Returns:
254
+ ndarray: Coefficients packed as ``[a, b, c, d]``.
255
+ """
256
+ x = np.pi * d_sphere * n_env / lambda0
257
+ m_rel = m_sphere / n_env
258
+ a, b, c, d = coefficients(m_rel, x, n_pole=n_pole, internal=True)
259
+ return np.array([a, b, c, d])
260
+
261
+
262
+ def _vectorized_field_eval(evaluator, r, theta, phi):
263
+ """Evaluate a spherical-field callback on scalar or broadcasted inputs.
264
+
265
+ Args:
266
+ evaluator (callable): Callable that accepts scalar ``(r, theta, phi)``
267
+ and returns a complex 3-vector.
268
+ r (float or ndarray): Radial coordinate(s).
269
+ theta (float or ndarray): Polar angle(s) in radians.
270
+ phi (float or ndarray): Azimuth angle(s) in radians.
138
271
 
139
272
  Returns:
140
- tuple: Electric field components (E_r, E_theta, E_phi).
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.
288
+
289
+ Args:
290
+ evaluator (callable): Callable that accepts scalar ``(r, theta, phi)``
291
+ and returns a tuple ``(E, H)``, each a complex 3-vector.
292
+ r (float or ndarray): Radial coordinate(s).
293
+ theta (float or ndarray): Polar angle(s) in radians.
294
+ phi (float or ndarray): Azimuth angle(s) in radians.
295
+
296
+ Returns:
297
+ tuple[ndarray, ndarray]: Tuple ``(E, H)`` with each array shaped
298
+ ``(3, ...)``.
299
+ """
300
+ rr, tt, pp = np.broadcast_arrays(np.asarray(r), np.asarray(theta), np.asarray(phi))
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.
316
+
317
+ Args:
318
+ x (float or ndarray): Cartesian x coordinate(s).
319
+ y (float or ndarray): Cartesian y coordinate(s).
320
+ z (float or ndarray): Cartesian z coordinate(s).
321
+
322
+ Returns:
323
+ tuple[ndarray, ndarray, ndarray]: ``(r, theta, phi)`` arrays after
324
+ broadcasting the inputs.
325
+ """
326
+ xx, yy, zz = np.broadcast_arrays(np.asarray(x), np.asarray(y), np.asarray(z))
327
+ rr = np.sqrt(xx**2 + yy**2 + zz**2)
328
+ with np.errstate(divide="ignore", invalid="ignore"):
329
+ cos_theta = np.where(rr == 0, 1.0, np.clip(zz / rr, -1.0, 1.0))
330
+ theta = np.arccos(cos_theta)
331
+ phi = np.arctan2(yy, xx)
332
+ return rr, theta, phi
333
+
334
+
335
+ def _spherical_components_to_cartesian(field_sph, r, theta, phi):
336
+ """Convert spherical vector components to Cartesian components.
337
+
338
+ Args:
339
+ field_sph (ndarray): Spherical components ``[F_r, F_theta, F_phi]``.
340
+ r (float or ndarray): Radial coordinate(s).
341
+ theta (float or ndarray): Polar angle(s) in radians.
342
+ phi (float or ndarray): Azimuth angle(s) in radians.
343
+
344
+ Returns:
345
+ ndarray: Cartesian components ``[F_x, F_y, F_z]``.
346
+ """
347
+ fx, fy, fz = spherical_vector_to_cartesian(field_sph[0], field_sph[1], field_sph[2], r, theta, phi)
348
+ return np.array([fx, fy, fz])
349
+
350
+
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]``.
141
367
  """
142
368
  a, b, c, d = abcd
143
369
 
@@ -145,49 +371,356 @@ def e_near(abcd, lambda0, d_sphere, m_index, r, theta, phi):
145
371
  nn = np.arange(1, N + 1)
146
372
  scale = 1j**nn * (2 * nn + 1) / ((nn + 1) * nn)
147
373
 
148
- M_rad, M_the, M_phi = M_odd_array(N, lambda0, d_sphere, m_index, r, theta, phi)
149
- N_rad, N_the, N_phi = N_even_array(N, lambda0, d_sphere, m_index, r, theta, phi)
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
150
390
 
151
- if r < d_sphere / 2:
152
- E_rad = np.sum(scale * (c * M_rad - 1j * d * N_rad))
153
- E_the = np.sum(scale * (c * M_the - 1j * d * N_the))
154
- E_phi = np.sum(scale * (c * M_phi - 1j * d * N_phi))
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)
155
395
  else:
156
- E_rad = np.sum(scale * (1j * a * N_rad - b * M_rad))
157
- E_the = np.sum(scale * (1j * a * N_the - b * M_the))
158
- E_phi = np.sum(scale * (1j * a * N_phi - b * M_phi))
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
+
159
408
 
160
- return np.array([E_rad, E_the, -E_phi])
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.
161
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.
162
422
 
163
- def e_plane(x, y, z, N=100):
423
+ Returns:
424
+ ndarray: Spherical magnetic components ``[H_r, H_theta, H_phi]``.
164
425
  """
165
- Calculate a plane wave using vector spherical harmonics.
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
+ def e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=True, n_pole=0, abcd=None):
527
+ """Calculate the electric field in and around a sphere.
528
+
529
+ Args:
530
+ lambda0 (float): Vacuum wavelength.
531
+ d_sphere (float): Sphere diameter.
532
+ m_sphere (complex): Sphere refractive index.
533
+ n_env (float): Refractive index of the surrounding medium.
534
+ r (float or ndarray): Radial coordinate(s).
535
+ theta (float or ndarray): Polar angle(s) in radians.
536
+ phi (float or ndarray): Azimuth angle(s) in radians.
537
+ include_incident (bool): Include incident field for points outside sphere.
538
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
539
+ abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
540
+ If provided, ``n_pole`` is ignored.
541
+
542
+ Returns:
543
+ ndarray: Spherical electric components ``[E_r, E_theta, E_phi]`` with
544
+ shape ``(3, ...)``.
545
+ """
546
+ if abcd is None:
547
+ abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
548
+
549
+ def evaluator(rr, tt, pp):
550
+ return _e_near_abcd(abcd, lambda0, d_sphere, m_sphere, n_env, rr, tt, pp, include_incident)
551
+
552
+ return _vectorized_field_eval(evaluator, r, theta, phi)
553
+
554
+
555
+ def h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident=True, n_pole=0, abcd=None):
556
+ """Calculate the magnetic field in and around a sphere.
557
+
558
+ Args:
559
+ lambda0 (float): Vacuum wavelength.
560
+ d_sphere (float): Sphere diameter.
561
+ m_sphere (complex): Sphere refractive index.
562
+ n_env (float): Refractive index of the surrounding medium.
563
+ r (float or ndarray): Radial coordinate(s).
564
+ theta (float or ndarray): Polar angle(s) in radians.
565
+ phi (float or ndarray): Azimuth angle(s) in radians.
566
+ include_incident (bool): Include incident field for points outside sphere.
567
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
568
+ abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
569
+ If provided, ``n_pole`` is ignored.
570
+
571
+ Returns:
572
+ ndarray: Spherical magnetic components ``[H_r, H_theta, H_phi]`` with
573
+ shape ``(3, ...)``.
574
+ """
575
+ if abcd is None:
576
+ abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
577
+
578
+ def evaluator(rr, tt, pp):
579
+ return _h_near_abcd(abcd, lambda0, d_sphere, m_sphere, n_env, rr, tt, pp, include_incident)
580
+
581
+ return _vectorized_field_eval(evaluator, r, theta, phi)
582
+
583
+
584
+ def eh_near(
585
+ lambda0,
586
+ d_sphere,
587
+ m_sphere,
588
+ n_env,
589
+ r,
590
+ theta,
591
+ phi,
592
+ include_incident=True,
593
+ n_pole=0,
594
+ abcd=None,
595
+ ):
596
+ """Calculate electric and magnetic fields in and around a sphere.
166
597
 
167
598
  Args:
168
- x: position
169
- y: position
170
- z: position
171
- N: number of points
599
+ lambda0 (float): Vacuum wavelength.
600
+ d_sphere (float): Sphere diameter.
601
+ m_sphere (complex): Sphere refractive index.
602
+ n_env (float): Refractive index of the surrounding medium.
603
+ r (float or ndarray): Radial coordinate(s).
604
+ theta (float or ndarray): Polar angle(s) in radians.
605
+ phi (float or ndarray): Azimuth angle(s) in radians.
606
+ include_incident (bool): Include incident field for points outside sphere.
607
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
608
+ abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
609
+ If provided, ``n_pole`` is ignored.
172
610
 
173
611
  Returns:
174
- tuple: Electric field components (E_z, E_y, E_z).
612
+ tuple[ndarray, ndarray]: Tuple ``(E, H)`` in spherical components,
613
+ each with shape ``(3, ...)``.
175
614
  """
176
- lambda0 = 1
177
- m_index = 1
615
+ if abcd is None:
616
+ abcd = _coefficients_abcd(lambda0, d_sphere, m_sphere, n_env, n_pole)
617
+
618
+ def evaluator(rr, tt, pp):
619
+ return _eh_near_abcd(abcd, lambda0, d_sphere, m_sphere, n_env, rr, tt, pp, include_incident)
620
+
621
+ return _vectorized_field_pair_eval(evaluator, r, theta, phi)
622
+
623
+
624
+ def e_near_cartesian(
625
+ lambda0,
626
+ d_sphere,
627
+ m_sphere,
628
+ n_env,
629
+ x,
630
+ y,
631
+ z,
632
+ include_incident=True,
633
+ n_pole=0,
634
+ abcd=None,
635
+ ):
636
+ """Calculate electric near field in Cartesian coordinates.
178
637
 
179
- n = np.arange(1, N + 1)
180
- scale = (1j) ** n * (2 * n + 1) / n / (n + 1)
638
+ Args:
639
+ lambda0 (float): Vacuum wavelength.
640
+ d_sphere (float): Sphere diameter.
641
+ m_sphere (complex): Sphere refractive index.
642
+ n_env (float): Refractive index of the surrounding medium.
643
+ x (float or ndarray): Cartesian x coordinate(s).
644
+ y (float or ndarray): Cartesian y coordinate(s).
645
+ z (float or ndarray): Cartesian z coordinate(s).
646
+ include_incident (bool): Include incident field for points outside sphere.
647
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
648
+ abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
181
649
 
182
- r, theta, phi = cartesian_to_spherical(x, y, z)
183
- d_sphere = 3 * r
650
+ Returns:
651
+ ndarray: Cartesian electric components ``[E_x, E_y, E_z]``.
652
+ """
653
+ r, theta, phi = _cartesian_to_spherical_safe(x, y, z)
654
+ e_sph = e_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
655
+ return _spherical_components_to_cartesian(e_sph, r, theta, phi)
184
656
 
185
- M_r, M_theta, M_phi = M_odd_array(N, lambda0, d_sphere, m_index, r, theta, phi)
186
- N_r, N_theta, N_phi = N_even_array(N, lambda0, d_sphere, m_index, r, theta, phi)
187
657
 
188
- E_r = np.sum(scale * (M_r - 1j * N_r))
189
- E_theta = np.sum(scale * (M_theta - 1j * N_theta))
190
- E_phi = np.sum(scale * (M_phi - 1j * N_phi))
658
+ def h_near_cartesian(
659
+ lambda0,
660
+ d_sphere,
661
+ m_sphere,
662
+ n_env,
663
+ x,
664
+ y,
665
+ z,
666
+ include_incident=True,
667
+ n_pole=0,
668
+ abcd=None,
669
+ ):
670
+ """Calculate magnetic near field in Cartesian coordinates.
671
+
672
+ Args:
673
+ lambda0 (float): Vacuum wavelength.
674
+ d_sphere (float): Sphere diameter.
675
+ m_sphere (complex): Sphere refractive index.
676
+ n_env (float): Refractive index of the surrounding medium.
677
+ x (float or ndarray): Cartesian x coordinate(s).
678
+ y (float or ndarray): Cartesian y coordinate(s).
679
+ z (float or ndarray): Cartesian z coordinate(s).
680
+ include_incident (bool): Include incident field for points outside sphere.
681
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
682
+ abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
191
683
 
192
- Ex, Ey, Ez = spherical_vector_to_cartesian(E_r, E_theta, E_phi, r, theta, phi)
193
- return Ex, Ey, Ez
684
+ Returns:
685
+ ndarray: Cartesian magnetic components ``[H_x, H_y, H_z]``.
686
+ """
687
+ r, theta, phi = _cartesian_to_spherical_safe(x, y, z)
688
+ h_sph = h_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
689
+ return _spherical_components_to_cartesian(h_sph, r, theta, phi)
690
+
691
+
692
+ def eh_near_cartesian(
693
+ lambda0,
694
+ d_sphere,
695
+ m_sphere,
696
+ n_env,
697
+ x,
698
+ y,
699
+ z,
700
+ include_incident=True,
701
+ n_pole=0,
702
+ abcd=None,
703
+ ):
704
+ """Calculate electric and magnetic near fields in Cartesian coordinates.
705
+
706
+ Args:
707
+ lambda0 (float): Vacuum wavelength.
708
+ d_sphere (float): Sphere diameter.
709
+ m_sphere (complex): Sphere refractive index.
710
+ n_env (float): Refractive index of the surrounding medium.
711
+ x (float or ndarray): Cartesian x coordinate(s).
712
+ y (float or ndarray): Cartesian y coordinate(s).
713
+ z (float or ndarray): Cartesian z coordinate(s).
714
+ include_incident (bool): Include incident field for points outside sphere.
715
+ n_pole (int): Requested multipole order. ``0`` means automatic truncation.
716
+ abcd (ndarray or None): Optional precomputed coefficients ``[a, b, c, d]``.
717
+
718
+ Returns:
719
+ tuple[ndarray, ndarray]: Tuple ``(E_xyz, H_xyz)`` where each array is
720
+ ``[x, y, z]`` components.
721
+ """
722
+ r, theta, phi = _cartesian_to_spherical_safe(x, y, z)
723
+ e_sph, h_sph = eh_near(lambda0, d_sphere, m_sphere, n_env, r, theta, phi, include_incident, n_pole, abcd)
724
+ e_xyz = _spherical_components_to_cartesian(e_sph, r, theta, phi)
725
+ h_xyz = _spherical_components_to_cartesian(h_sph, r, theta, phi)
726
+ return e_xyz, h_xyz