pystratify 0.10.2__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.
- pystratify/PLANAR-LICENSE.txt +7 -0
- pystratify/__init__.py +125 -0
- pystratify/casimir.py +97 -0
- pystratify/chiral.py +426 -0
- pystratify/convergence.py +48 -0
- pystratify/cylinder_emission.py +264 -0
- pystratify/cylindrical.py +177 -0
- pystratify/decay.py +754 -0
- pystratify/drude.py +82 -0
- pystratify/emission.py +617 -0
- pystratify/energy.py +274 -0
- pystratify/ensemble.py +180 -0
- pystratify/farfield.py +408 -0
- pystratify/focused.py +1287 -0
- pystratify/integration.py +43 -0
- pystratify/nearfield.py +192 -0
- pystratify/normalized.py +798 -0
- pystratify/observables.py +192 -0
- pystratify/pairs.py +208 -0
- pystratify/planar.py +1812 -0
- pystratify/planar_emission.py +232 -0
- pystratify/problem.py +152 -0
- pystratify/rates.py +1334 -0
- pystratify/references.py +527 -0
- pystratify/response.py +116 -0
- pystratify/riccati.py +171 -0
- pystratify/sheets.py +239 -0
- pystratify/solver.py +324 -0
- pystratify/special.py +50 -0
- pystratify-0.10.2.dist-info/METADATA +616 -0
- pystratify-0.10.2.dist-info/RECORD +34 -0
- pystratify-0.10.2.dist-info/WHEEL +5 -0
- pystratify-0.10.2.dist-info/licenses/LICENSE +674 -0
- pystratify-0.10.2.dist-info/top_level.txt +1 -0
pystratify/farfield.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Far field under plane-wave illumination, vectorised over wavelengths.
|
|
2
|
+
|
|
3
|
+
Cross sections, scattering amplitudes, the full amplitude and Mueller
|
|
4
|
+
matrices, helicity-resolved cross sections, and angle-resolved scattering
|
|
5
|
+
patterns with directivity for any incident polarisation.
|
|
6
|
+
|
|
7
|
+
Conventions (Bohren & Huffman, chs. 3-4): the incident wave travels along +z
|
|
8
|
+
with Jones vector (E_x, E_y) and time dependence exp(-i omega t). In the far
|
|
9
|
+
zone E_sca = exp(ikr) / (-ikr) X(theta, phi), with
|
|
10
|
+
|
|
11
|
+
(X_par, X_perp) = [[S2, S3], [S4, S1]] (E_par, E_perp),
|
|
12
|
+
E_par = E_x cos(phi) + E_y sin(phi), E_perp = E_x sin(phi) - E_y cos(phi),
|
|
13
|
+
X_theta = X_par, X_phi = -X_perp,
|
|
14
|
+
|
|
15
|
+
so that dsigma/dOmega = |X|^2 / k^2. Everything here is computed from the
|
|
16
|
+
T-matrix blocks ``sol.t_matrix`` (W, L, 2, 2) in the (TM, TE) basis: diagonal
|
|
17
|
+
for achiral spheres (S3 = S4 = 0); chiral shells couple TM and TE and give
|
|
18
|
+
S3 = -S4.
|
|
19
|
+
|
|
20
|
+
Helicity: +1 is the field (x + iy)/sqrt(2) exp(ikz) - left-circular in the
|
|
21
|
+
optics convention (Jackson, sec. 7.2) - and -1 is (x - iy)/sqrt(2) exp(ikz).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from .solver import TE, TM, Solution, _partial_waves
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CrossSections",
|
|
34
|
+
"cross_sections",
|
|
35
|
+
"scattering_amplitudes",
|
|
36
|
+
"angular_functions",
|
|
37
|
+
"amplitude_matrix",
|
|
38
|
+
"mueller_matrix",
|
|
39
|
+
"HelicityCrossSections",
|
|
40
|
+
"helicity_cross_sections",
|
|
41
|
+
"ScatteringPattern",
|
|
42
|
+
"scattering_pattern",
|
|
43
|
+
"HELICITY",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
#: helicity of index 0 and 1 of every helicity axis
|
|
47
|
+
HELICITY = (+1, -1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class CrossSections:
|
|
52
|
+
"""Cross sections (length^2) for every wavelength of the batch.
|
|
53
|
+
|
|
54
|
+
``*_by_order`` have shape ``(W, 2, L)``: axis 1 is the polarisation
|
|
55
|
+
(``TM`` = electric multipoles, ``TE`` = magnetic). Totals have shape
|
|
56
|
+
``(W,)``; ``q_*`` are efficiencies, normalised to pi R^2 of the outer radius.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
orders: np.ndarray
|
|
60
|
+
sca_by_order: np.ndarray
|
|
61
|
+
ext_by_order: np.ndarray
|
|
62
|
+
abs_by_order: np.ndarray
|
|
63
|
+
geometric: float
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def sca(self) -> np.ndarray:
|
|
67
|
+
return self.sca_by_order.sum(axis=(-2, -1))
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def ext(self) -> np.ndarray:
|
|
71
|
+
return self.ext_by_order.sum(axis=(-2, -1))
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def abs(self) -> np.ndarray:
|
|
75
|
+
return self.abs_by_order.sum(axis=(-2, -1))
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def q_sca(self) -> np.ndarray:
|
|
79
|
+
return self.sca / self.geometric
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def q_ext(self) -> np.ndarray:
|
|
83
|
+
return self.ext / self.geometric
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def q_abs(self) -> np.ndarray:
|
|
87
|
+
return self.abs / self.geometric
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _achiral(sol, name):
|
|
91
|
+
if not isinstance(sol, Solution):
|
|
92
|
+
raise TypeError(f"{name} needs an achiral Solution; for chiral spheres use helicity_cross_sections")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cross_sections(sol: Solution) -> CrossSections:
|
|
96
|
+
"""Scattering, extinction and absorption cross sections.
|
|
97
|
+
|
|
98
|
+
The real part of the host wavenumber is used, so an absorbing host is
|
|
99
|
+
handled only approximately.
|
|
100
|
+
"""
|
|
101
|
+
_achiral(sol, "cross_sections")
|
|
102
|
+
l = sol.orders
|
|
103
|
+
k = sol.k[:, -1][:, None, None]
|
|
104
|
+
t = np.moveaxis(sol.t, 0, 1) # (W, 2, L)
|
|
105
|
+
weight = 2 * np.pi * (2 * l + 1) / k.real
|
|
106
|
+
sca = weight / k.real * np.abs(t) ** 2
|
|
107
|
+
ext = -weight * (t / k).real
|
|
108
|
+
return CrossSections(
|
|
109
|
+
orders=l,
|
|
110
|
+
sca_by_order=sca,
|
|
111
|
+
ext_by_order=ext,
|
|
112
|
+
abs_by_order=ext - sca,
|
|
113
|
+
geometric=float(np.pi * sol.radii[-1] ** 2),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def angular_functions(orders, theta):
|
|
118
|
+
"""Bohren-Huffman pi_l = P_l^1 / sin(theta) and tau_l = dP_l^1 / d(theta).
|
|
119
|
+
|
|
120
|
+
Upward recurrence in cos(theta), regular at theta = 0 and pi. Returns two
|
|
121
|
+
arrays of shape ``(len(orders), len(theta))``.
|
|
122
|
+
"""
|
|
123
|
+
orders = np.asarray(orders)
|
|
124
|
+
c = np.cos(np.atleast_1d(np.asarray(theta, dtype=float)))
|
|
125
|
+
top = int(orders.max())
|
|
126
|
+
pi = np.zeros((top + 1, c.size))
|
|
127
|
+
tau = np.zeros((top + 1, c.size))
|
|
128
|
+
pi[1] = 1.0
|
|
129
|
+
tau[1] = c
|
|
130
|
+
for n in range(2, top + 1):
|
|
131
|
+
pi[n] = ((2 * n - 1) * c * pi[n - 1] - n * pi[n - 2]) / (n - 1)
|
|
132
|
+
tau[n] = n * c * pi[n] - (n + 1) * pi[n - 1]
|
|
133
|
+
return pi[orders], tau[orders]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def scattering_amplitudes(sol: Solution, theta, orders=None, polarisations=(TM, TE)):
|
|
137
|
+
"""Scattering amplitudes (S_par, S_per), each of shape ``(W, len(theta))``.
|
|
138
|
+
|
|
139
|
+
S_par = -S2 and S_per = -S1 in Bohren & Huffman's notation, the convention
|
|
140
|
+
of Rasskazov, Carney & Moroz (2020); intensities and Stokes parameters are
|
|
141
|
+
unaffected by the sign. :func:`amplitude_matrix` gives S1..S4 themselves.
|
|
142
|
+
|
|
143
|
+
``orders`` and ``polarisations`` (:data:`TM`, the electric multipoles a_l;
|
|
144
|
+
:data:`TE`, the magnetic b_l) keep only those partial waves, as in
|
|
145
|
+
:func:`near_field`: the far field of disjoint selections adds up amplitude
|
|
146
|
+
by amplitude, not in |S|^2, which carries their interference.
|
|
147
|
+
"""
|
|
148
|
+
_achiral(sol, "scattering_amplitudes")
|
|
149
|
+
l = sol.orders
|
|
150
|
+
pi, tau = angular_functions(l, theta)
|
|
151
|
+
weight = (2 * l + 1) / (l * (l + 1))
|
|
152
|
+
kept = _partial_waves(l, orders, polarisations)
|
|
153
|
+
tm, te = (1.0, 1.0) if kept is None else (kept[TM], kept[TE])
|
|
154
|
+
a, b = sol.a * weight * tm, sol.b * weight * te
|
|
155
|
+
return -(a @ tau + b @ pi), -(a @ pi + b @ tau)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _host_k(sol) -> np.ndarray:
|
|
159
|
+
"""Real host wavenumber per wavelength (as :func:`cross_sections`)."""
|
|
160
|
+
return sol.k[:, -1].real
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def amplitude_matrix(sol, theta):
|
|
164
|
+
"""Bohren-Huffman amplitude scattering matrix (S1, S2, S3, S4).
|
|
165
|
+
|
|
166
|
+
``sol``: :class:`~pystratify.Solution` or :class:`~pystratify.ChiralSolution`.
|
|
167
|
+
Each element has shape ``(W, len(theta))``.
|
|
168
|
+
"""
|
|
169
|
+
l = sol.orders
|
|
170
|
+
theta = np.atleast_1d(np.asarray(theta, dtype=float))
|
|
171
|
+
unique, index = np.unique(theta, return_inverse=True)
|
|
172
|
+
pi, tau = angular_functions(l, unique)
|
|
173
|
+
w = (2 * l + 1) / (l * (l + 1))
|
|
174
|
+
t = sol.t_matrix * w[:, None, None] # (W, L, 2, 2)
|
|
175
|
+
tm_tm, tm_te, te_tm, te_te = t[..., 0, 0], t[..., 0, 1], t[..., 1, 0], t[..., 1, 1]
|
|
176
|
+
s1 = -(tm_tm @ pi + te_te @ tau)
|
|
177
|
+
s2 = -(tm_tm @ tau + te_te @ pi)
|
|
178
|
+
s3 = -1j * (tm_te @ tau + te_tm @ pi)
|
|
179
|
+
s4 = 1j * (tm_te @ pi + te_tm @ tau)
|
|
180
|
+
return tuple(s[:, index] for s in (s1, s2, s3, s4))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
#: Stokes vector (I, Q, U, V) from the coherency vector (E_par E_par*, E_par E_perp*, E_perp E_par*, E_perp E_perp*)
|
|
184
|
+
_STOKES = np.array([[1, 0, 0, 1], [1, 0, 0, -1], [0, 1, 1, 0], [0, 1j, -1j, 0]])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def mueller_matrix(sol, theta):
|
|
188
|
+
"""Mueller matrix, shape ``(W, len(theta), 4, 4)``, in Bohren & Huffman's
|
|
189
|
+
convention (their Eq. 3.16 and Stokes vector, prefactor 1/(k r)^2 omitted):
|
|
190
|
+
for an achiral sphere M11 = (|S1|^2 + |S2|^2)/2, M12 = (|S2|^2 - |S1|^2)/2,
|
|
191
|
+
M33 = Re(S1 S2*), M34 = Im(S2 S1*).
|
|
192
|
+
"""
|
|
193
|
+
s1, s2, s3, s4 = amplitude_matrix(sol, theta)
|
|
194
|
+
jones = np.stack([np.stack([s2, s3], -1), np.stack([s4, s1], -1)], -2) # (W, T, 2, 2)
|
|
195
|
+
coherency = np.einsum("...ik,...jl->...ijkl", jones, np.conj(jones)).reshape(jones.shape[:-2] + (4, 4))
|
|
196
|
+
return np.real(_STOKES @ coherency @ np.linalg.inv(_STOKES))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class HelicityCrossSections:
|
|
201
|
+
"""Cross sections for circularly polarised illumination.
|
|
202
|
+
|
|
203
|
+
``*_by_order`` have shape ``(W, 2, L)`` with axis 1 the helicity
|
|
204
|
+
(index 0: +1, left-circular in the optics convention; index 1: -1).
|
|
205
|
+
Totals ``ext``, ``sca``, ``abs`` have shape ``(W, 2)``; linear or
|
|
206
|
+
unpolarised light gives their mean. ``cd_*`` = (+1) - (-1) and
|
|
207
|
+
``g_*`` = 2 cd / ((+1) + (-1)) are the circular dichroism and the
|
|
208
|
+
dissymmetry factor of extinction, scattering and absorption.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
orders: np.ndarray
|
|
212
|
+
sca_by_order: np.ndarray
|
|
213
|
+
ext_by_order: np.ndarray
|
|
214
|
+
abs_by_order: np.ndarray
|
|
215
|
+
geometric: float
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def sca(self) -> np.ndarray:
|
|
219
|
+
return self.sca_by_order.sum(axis=-1)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def ext(self) -> np.ndarray:
|
|
223
|
+
return self.ext_by_order.sum(axis=-1)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def abs(self) -> np.ndarray:
|
|
227
|
+
return self.abs_by_order.sum(axis=-1)
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def q_sca(self) -> np.ndarray:
|
|
231
|
+
return self.sca / self.geometric
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def q_ext(self) -> np.ndarray:
|
|
235
|
+
return self.ext / self.geometric
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def q_abs(self) -> np.ndarray:
|
|
239
|
+
return self.abs / self.geometric
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _cd(v):
|
|
243
|
+
return v[..., 0] - v[..., 1]
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _g(v):
|
|
247
|
+
return 2 * (v[..., 0] - v[..., 1]) / (v[..., 0] + v[..., 1])
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def cd_ext(self) -> np.ndarray:
|
|
251
|
+
return self._cd(self.ext)
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def cd_sca(self) -> np.ndarray:
|
|
255
|
+
return self._cd(self.sca)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def cd_abs(self) -> np.ndarray:
|
|
259
|
+
return self._cd(self.abs)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def g_ext(self) -> np.ndarray:
|
|
263
|
+
return self._g(self.ext)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def g_sca(self) -> np.ndarray:
|
|
267
|
+
return self._g(self.sca)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def g_abs(self) -> np.ndarray:
|
|
271
|
+
return self._g(self.abs)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
#: (TM, TE) amplitudes of the multipoles in a plane wave of helicity +1 and -1 (up to a common factor)
|
|
275
|
+
_HELICITY_VECTORS = np.array([[1.0, 1.0], [1.0, -1.0]])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def helicity_cross_sections(sol) -> HelicityCrossSections:
|
|
279
|
+
"""Extinction, scattering and absorption for both circular polarisations.
|
|
280
|
+
|
|
281
|
+
Works for achiral (identical helicities) and chiral spheres. A plane
|
|
282
|
+
wave of helicity s excites only m = s multipoles with (TM, TE) amplitudes
|
|
283
|
+
proportional to (1, s), so with T the (TM, TE) T-matrix block of order l
|
|
284
|
+
|
|
285
|
+
C_ext = -(2 pi / k^2) sum (2l + 1) Re[v^T T v],
|
|
286
|
+
C_sca = (2 pi / k^2) sum (2l + 1) |T v|^2, v = (1, s).
|
|
287
|
+
"""
|
|
288
|
+
l = sol.orders
|
|
289
|
+
k = _host_k(sol)[:, None, None]
|
|
290
|
+
t = sol.t_matrix # (W, L, 2, 2)
|
|
291
|
+
tv = np.einsum("wlij,hj->whli", t, _HELICITY_VECTORS) # (W, 2, L, 2)
|
|
292
|
+
weight = 2 * np.pi * (2 * l + 1) / k**2
|
|
293
|
+
ext = -weight * np.einsum("hi,whli->whl", _HELICITY_VECTORS, tv).real
|
|
294
|
+
sca = weight * np.sum(np.abs(tv) ** 2, axis=-1)
|
|
295
|
+
return HelicityCrossSections(
|
|
296
|
+
orders=l,
|
|
297
|
+
sca_by_order=sca,
|
|
298
|
+
ext_by_order=ext,
|
|
299
|
+
abs_by_order=ext - sca,
|
|
300
|
+
geometric=float(np.pi * sol.radii[-1] ** 2),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class FarFieldPolarization:
|
|
305
|
+
"""Polarisation analysis shared by far-field patterns (fields ``e_theta``, ``e_phi``)."""
|
|
306
|
+
|
|
307
|
+
e_theta: np.ndarray
|
|
308
|
+
e_phi: np.ndarray
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def intensity(self) -> np.ndarray:
|
|
312
|
+
"""|e_theta|^2 + |e_phi|^2."""
|
|
313
|
+
return np.abs(self.e_theta) ** 2 + np.abs(self.e_phi) ** 2
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def helicity(self) -> tuple:
|
|
317
|
+
"""Amplitudes (e_+, e_-) on (theta_hat +- i phi_hat)/sqrt(2): helicity +1 and -1 along the outgoing direction."""
|
|
318
|
+
return (self.e_theta - 1j * self.e_phi) / np.sqrt(2), (self.e_theta + 1j * self.e_phi) / np.sqrt(2)
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def circular_polarization(self) -> np.ndarray:
|
|
322
|
+
"""Degree of circular polarisation (|e_+|^2 - |e_-|^2) / (|e_+|^2 + |e_-|^2), +1 for pure helicity +1."""
|
|
323
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
324
|
+
return -2 * np.imag(self.e_theta * np.conj(self.e_phi)) / self.intensity
|
|
325
|
+
|
|
326
|
+
def stokes(self) -> np.ndarray:
|
|
327
|
+
"""Stokes parameters (I, Q, U, V) stacked on the last axis, in Bohren &
|
|
328
|
+
Huffman's convention (par = theta_hat, perp = -phi_hat, as :func:`mueller_matrix`);
|
|
329
|
+
note their V = -I x ``circular_polarization``."""
|
|
330
|
+
par, perp = self.e_theta, -self.e_phi
|
|
331
|
+
return np.stack(
|
|
332
|
+
[
|
|
333
|
+
np.abs(par) ** 2 + np.abs(perp) ** 2,
|
|
334
|
+
np.abs(par) ** 2 - np.abs(perp) ** 2,
|
|
335
|
+
2 * np.real(par * np.conj(perp)),
|
|
336
|
+
-2 * np.imag(par * np.conj(perp)),
|
|
337
|
+
],
|
|
338
|
+
axis=-1,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(frozen=True)
|
|
343
|
+
class ScatteringPattern(FarFieldPolarization):
|
|
344
|
+
"""Scattered far field for one incident polarisation.
|
|
345
|
+
|
|
346
|
+
``e_theta``, ``e_phi``: components of the vector amplitude X (E_sca =
|
|
347
|
+
exp(ikr)/(-ikr) X for a unit incident field), shape ``(W,) + S`` with S
|
|
348
|
+
the broadcast shape of ``theta`` and ``phi``. ``sca``: scattering cross
|
|
349
|
+
section for this polarisation, ``(W,)``, from the multipole coefficients.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
theta: np.ndarray
|
|
353
|
+
phi: np.ndarray
|
|
354
|
+
e_theta: np.ndarray
|
|
355
|
+
e_phi: np.ndarray
|
|
356
|
+
k: np.ndarray
|
|
357
|
+
sca: np.ndarray
|
|
358
|
+
polarization: np.ndarray
|
|
359
|
+
|
|
360
|
+
def _per_wavelength(self, v):
|
|
361
|
+
return v.reshape(v.shape + (1,) * self.theta.ndim)
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def differential_cross_section(self) -> np.ndarray:
|
|
365
|
+
"""dsigma/dOmega = |X|^2 / k^2 (length^2 per steradian)."""
|
|
366
|
+
return self.intensity / self._per_wavelength(self.k) ** 2
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def directivity(self) -> np.ndarray:
|
|
370
|
+
"""4 pi (dsigma/dOmega) / sigma_sca: 1 for isotropic scattering, integrates to 4 pi."""
|
|
371
|
+
return 4 * np.pi * self.differential_cross_section / self._per_wavelength(self.sca)
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def phase_function(self) -> np.ndarray:
|
|
375
|
+
"""(dsigma/dOmega) / sigma_sca, normalised to 1 over the sphere."""
|
|
376
|
+
return self.directivity / (4 * np.pi)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _jones(polarization):
|
|
380
|
+
p = np.asarray(polarization, dtype=complex).ravel()
|
|
381
|
+
norm = np.linalg.norm(p)
|
|
382
|
+
if p.shape != (2,) or not np.isfinite(norm) or norm == 0:
|
|
383
|
+
raise ValueError("polarization must be a nonzero Jones vector (E_x, E_y)")
|
|
384
|
+
return p / norm
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def scattering_pattern(sol, theta, phi=0.0, polarization=(1.0, 0.0)) -> ScatteringPattern:
|
|
388
|
+
"""Angle-resolved scattered far field, differential cross section and directivity.
|
|
389
|
+
|
|
390
|
+
``sol``: :class:`~pystratify.Solution` or :class:`~pystratify.ChiralSolution`.
|
|
391
|
+
``theta``, ``phi``: scattering directions (broadcast together; theta from
|
|
392
|
+
the propagation direction +z). ``polarization``: incident Jones vector
|
|
393
|
+
(E_x, E_y), normalised internally; e.g. (1, 1j) for helicity +1.
|
|
394
|
+
"""
|
|
395
|
+
jones = _jones(polarization)
|
|
396
|
+
theta, phi = np.broadcast_arrays(np.asarray(theta, dtype=float), np.asarray(phi, dtype=float))
|
|
397
|
+
s1, s2, s3, s4 = amplitude_matrix(sol, theta.ravel())
|
|
398
|
+
cp, sp = np.cos(phi.ravel()), np.sin(phi.ravel())
|
|
399
|
+
e_par = jones[0] * cp + jones[1] * sp
|
|
400
|
+
e_perp = jones[0] * sp - jones[1] * cp
|
|
401
|
+
shape = (s1.shape[0],) + theta.shape
|
|
402
|
+
x_theta = (s2 * e_par + s3 * e_perp).reshape(shape)
|
|
403
|
+
x_phi = -(s4 * e_par + s1 * e_perp).reshape(shape)
|
|
404
|
+
helicity = np.array([jones[0] - 1j * jones[1], jones[0] + 1j * jones[1]]) / np.sqrt(2)
|
|
405
|
+
sca = helicity_cross_sections(sol).sca @ np.abs(helicity) ** 2
|
|
406
|
+
return ScatteringPattern(
|
|
407
|
+
theta=theta, phi=phi, e_theta=x_theta, e_phi=x_phi, k=_host_k(sol), sca=sca, polarization=jones
|
|
408
|
+
)
|