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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (C) 2012-2024 Steven Byrnes
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
pystratify/__init__.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""PyStratify: light scattering by multilayered (stratified) spheres.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pystratify as ps
|
|
7
|
+
|
|
8
|
+
wavelength = np.linspace(500, 900, 401) # nm
|
|
9
|
+
radii = [50, 55] # nm: SiO2 core, Au shell
|
|
10
|
+
n_au = ... # (401,) complex, e.g. from refractiveindex.info
|
|
11
|
+
n = np.stack([np.full(401, 1.45), n_au, np.full(401, 1.33)], axis=1) # host last
|
|
12
|
+
sol = ps.solve(radii, n, wavelength)
|
|
13
|
+
ps.cross_sections(sol).q_ext # (401,)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .casimir import CasimirPolder, casimir_polder
|
|
17
|
+
from .chiral import ChiralSolution, solve_chiral
|
|
18
|
+
from .convergence import truncation_order
|
|
19
|
+
from .decay import DecayRates, decay_rates, locate_shell
|
|
20
|
+
from .drude import DRUDE, DrudeModel, free_path_correction, surface_damping_wavelength
|
|
21
|
+
from .emission import EmissionPattern, dipole_far_field, source_covariance
|
|
22
|
+
from .ensemble import ShellAverage, SpectralDensity, shell_average, spectral_density
|
|
23
|
+
from .energy import (
|
|
24
|
+
EnergyDensity,
|
|
25
|
+
ShellEnergy,
|
|
26
|
+
electric_prefactor,
|
|
27
|
+
energy_density,
|
|
28
|
+
energy_prefactors,
|
|
29
|
+
shell_energy,
|
|
30
|
+
)
|
|
31
|
+
from .farfield import (
|
|
32
|
+
CrossSections,
|
|
33
|
+
HelicityCrossSections,
|
|
34
|
+
ScatteringPattern,
|
|
35
|
+
amplitude_matrix,
|
|
36
|
+
angular_functions,
|
|
37
|
+
cross_sections,
|
|
38
|
+
helicity_cross_sections,
|
|
39
|
+
mueller_matrix,
|
|
40
|
+
scattering_amplitudes,
|
|
41
|
+
scattering_pattern,
|
|
42
|
+
)
|
|
43
|
+
from .nearfield import NearField, near_field
|
|
44
|
+
from .pairs import GreenDyadic, green_dyadic
|
|
45
|
+
from .normalized import NormalizedRates, NormalizedTerms, normalized_decay_rates, normalized_terms
|
|
46
|
+
from .rates import EmissionRates, emission_rates
|
|
47
|
+
from .sheets import Feibelman, Sheet, graphene_conductivity
|
|
48
|
+
from .riccati import log_riccati
|
|
49
|
+
from .solver import TE, TM, Solution, solve
|
|
50
|
+
from .problem import Problem, PlaneWave, PointDipole, FocusedBeam
|
|
51
|
+
from .observables import solve_problem, solve_focused
|
|
52
|
+
from .focused import (
|
|
53
|
+
FocusedField,
|
|
54
|
+
FocusedResult,
|
|
55
|
+
focal_field,
|
|
56
|
+
focused_cylinders,
|
|
57
|
+
focused_field_cylinders,
|
|
58
|
+
focused_field_films,
|
|
59
|
+
focused_field_spheres,
|
|
60
|
+
focused_films,
|
|
61
|
+
focused_spheres,
|
|
62
|
+
)
|
|
63
|
+
from .cylindrical import solve_cylinder, CylinderSolution, cross_widths, cylinder_pattern
|
|
64
|
+
|
|
65
|
+
__version__ = "0.10.2"
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"Problem", "PlaneWave", "PointDipole", "FocusedBeam", "solve_problem", "solve_focused",
|
|
69
|
+
"focused_films", "focused_spheres", "focused_cylinders", "FocusedResult",
|
|
70
|
+
"focused_field_films", "focused_field_spheres", "focused_field_cylinders", "FocusedField", "focal_field",
|
|
71
|
+
"solve_cylinder", "CylinderSolution", "cross_widths", "cylinder_pattern",
|
|
72
|
+
"solve",
|
|
73
|
+
"Solution",
|
|
74
|
+
"solve_chiral",
|
|
75
|
+
"ChiralSolution",
|
|
76
|
+
"TM",
|
|
77
|
+
"TE",
|
|
78
|
+
"truncation_order",
|
|
79
|
+
"cross_sections",
|
|
80
|
+
"CrossSections",
|
|
81
|
+
"helicity_cross_sections",
|
|
82
|
+
"HelicityCrossSections",
|
|
83
|
+
"scattering_amplitudes",
|
|
84
|
+
"amplitude_matrix",
|
|
85
|
+
"mueller_matrix",
|
|
86
|
+
"scattering_pattern",
|
|
87
|
+
"ScatteringPattern",
|
|
88
|
+
"dipole_far_field",
|
|
89
|
+
"EmissionPattern",
|
|
90
|
+
"source_covariance",
|
|
91
|
+
"angular_functions",
|
|
92
|
+
"near_field",
|
|
93
|
+
"NearField",
|
|
94
|
+
"energy_density",
|
|
95
|
+
"EnergyDensity",
|
|
96
|
+
"shell_energy",
|
|
97
|
+
"ShellEnergy",
|
|
98
|
+
"electric_prefactor",
|
|
99
|
+
"energy_prefactors",
|
|
100
|
+
"decay_rates",
|
|
101
|
+
"DecayRates",
|
|
102
|
+
"emission_rates",
|
|
103
|
+
"EmissionRates",
|
|
104
|
+
"locate_shell",
|
|
105
|
+
"Feibelman",
|
|
106
|
+
"Sheet",
|
|
107
|
+
"graphene_conductivity",
|
|
108
|
+
"DRUDE",
|
|
109
|
+
"DrudeModel",
|
|
110
|
+
"free_path_correction",
|
|
111
|
+
"surface_damping_wavelength",
|
|
112
|
+
"log_riccati",
|
|
113
|
+
"normalized_decay_rates",
|
|
114
|
+
"casimir_polder",
|
|
115
|
+
"CasimirPolder",
|
|
116
|
+
"green_dyadic",
|
|
117
|
+
"GreenDyadic",
|
|
118
|
+
"shell_average",
|
|
119
|
+
"ShellAverage",
|
|
120
|
+
"spectral_density",
|
|
121
|
+
"SpectralDensity",
|
|
122
|
+
"normalized_terms",
|
|
123
|
+
"NormalizedTerms",
|
|
124
|
+
"NormalizedRates",
|
|
125
|
+
]
|
pystratify/casimir.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Casimir-Polder potential of a ground-state atom in or near a multilayered sphere.
|
|
2
|
+
|
|
3
|
+
For an isotropic electric polarizability alpha(i xi) (Buhmann & Welsch, Prog. Quantum Electron. 31,
|
|
4
|
+
51 (2007), Eq. (110)),
|
|
5
|
+
|
|
6
|
+
U(r) = (hbar mu_0 / 2 pi) int_0^inf dxi xi^2 alpha(i xi) Tr G_s(r, r, i xi),
|
|
7
|
+
|
|
8
|
+
with the scattered Green's dyadic at imaginary frequency. The trace is the dipole series of the
|
|
9
|
+
normalized formulation at k = i xi n(i xi)/c, Tr G_s = (i k / 6 pi)(g_perp + 2 g_par) (shell
|
|
10
|
+
normalization, as for the decay rates and the shift), and with alpha = 4 pi eps_0 alpha_V in volume
|
|
11
|
+
units
|
|
12
|
+
|
|
13
|
+
U(r) = -(hbar / 3 pi c^3) int_0^inf dxi xi^3 n_d(i xi) alpha_V(i xi) [g_perp + 2 g_par](i xi).
|
|
14
|
+
|
|
15
|
+
At imaginary frequency every auxiliary function is real up to a fixed phase (P real, A and B
|
|
16
|
+
imaginary, jbar real), the Hankel-type solution decays like exp(-kappa r), and the normalized
|
|
17
|
+
series converge geometrically at the emitter as for real frequencies; nothing can overflow.
|
|
18
|
+
The integral is done by Gauss-Legendre quadrature in ln xi.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from .energy import gauss_legendre
|
|
28
|
+
from .normalized import L_CAP, _complex_converged, _dipole_series, _Sweep
|
|
29
|
+
from .convergence import orders_needed
|
|
30
|
+
|
|
31
|
+
__all__ = ["CasimirPolder", "casimir_polder"]
|
|
32
|
+
|
|
33
|
+
HBAR = 1.054571817e-34 # J s
|
|
34
|
+
C_NM = 2.99792458e17 # speed of light, nm/s
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class CasimirPolder:
|
|
39
|
+
"""Casimir-Polder potential ``energy`` (J) at the radii ``r``; ``xi`` and ``integrand`` (P, X) are the
|
|
40
|
+
quadrature nodes (rad/s) and xi^3 n_d alpha_V [g_perp + 2 g_par], ``orders`` the most multipoles used."""
|
|
41
|
+
|
|
42
|
+
r: np.ndarray
|
|
43
|
+
energy: np.ndarray
|
|
44
|
+
xi: np.ndarray
|
|
45
|
+
integrand: np.ndarray
|
|
46
|
+
orders: int
|
|
47
|
+
converged: np.ndarray
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def frequency(self) -> np.ndarray:
|
|
51
|
+
"""U / h in Hz."""
|
|
52
|
+
return self.energy / (2 * np.pi * HBAR)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def casimir_polder(radii, eps, r, polarizability, mu=None, xi_range=(1e9, 1e19), nodes=96, tol=1e-10, l_cap=L_CAP):
|
|
56
|
+
"""Casimir-Polder potential of an atom at radius (radii) ``r`` (nm), in any layer.
|
|
57
|
+
|
|
58
|
+
``eps(xi)`` and ``mu(xi)`` (optional) return the (N + 1,) permittivities and permeabilities of the
|
|
59
|
+
layers at imaginary frequency i xi (xi in rad/s; real and >= 1 for passive media), host last.
|
|
60
|
+
``polarizability(xi)`` is the atom's isotropic polarizability alpha(i xi) / (4 pi eps_0) in nm^3.
|
|
61
|
+
The integral over xi runs over ``xi_range`` with ``nodes`` Gauss-Legendre nodes in ln xi; every
|
|
62
|
+
order sum meets ``tol`` (automatic truncation, at most ``l_cap`` orders). Lengths in nm.
|
|
63
|
+
"""
|
|
64
|
+
radii = np.atleast_1d(np.asarray(radii, dtype=float))
|
|
65
|
+
r = np.atleast_1d(np.asarray(r, dtype=float))
|
|
66
|
+
if np.any(~(r > 0)) or np.any(np.isin(r, radii)):
|
|
67
|
+
raise ValueError("positions must be positive and off the interfaces")
|
|
68
|
+
shells = np.searchsorted(radii, r, side="right")
|
|
69
|
+
t, w = gauss_legendre(nodes)
|
|
70
|
+
lo, hi = np.log(xi_range[0]), np.log(xi_range[1])
|
|
71
|
+
ln_xi = 0.5 * (hi - lo) * t + 0.5 * (hi + lo)
|
|
72
|
+
xi, w = np.exp(ln_xi), 0.5 * (hi - lo) * w * np.exp(ln_xi) # d xi = xi d ln xi
|
|
73
|
+
q = np.max(np.minimum(radii[None, :] / r[:, None], r[:, None] / radii[None, :]))
|
|
74
|
+
integrand = np.zeros((r.size, xi.size))
|
|
75
|
+
ok = np.ones(r.size, dtype=bool)
|
|
76
|
+
used = 0
|
|
77
|
+
for i, x in enumerate(xi):
|
|
78
|
+
e = np.atleast_1d(np.asarray(eps(x), dtype=complex))
|
|
79
|
+
m = np.ones_like(e) if mu is None else np.atleast_1d(np.asarray(mu(x), dtype=complex))
|
|
80
|
+
if e.shape != (radii.size + 1,) or m.shape != e.shape:
|
|
81
|
+
raise ValueError(f"eps and mu must return ({radii.size + 1},) arrays, host last")
|
|
82
|
+
n = np.sqrt(e * m).real + 0j # real at imaginary frequency for passive media
|
|
83
|
+
k = 1j * (x / C_NM) * n
|
|
84
|
+
L = int(min(l_cap, max(32, orders_needed(q, tol, l_cap))))
|
|
85
|
+
while True:
|
|
86
|
+
terms = _Sweep(radii, n, m, k, L).at(r)
|
|
87
|
+
g, _ = _dipole_series(terms, "electric")
|
|
88
|
+
good = _complex_converged(g, tol)
|
|
89
|
+
if good.all() or L >= l_cap:
|
|
90
|
+
break
|
|
91
|
+
L = min(2 * L, l_cap)
|
|
92
|
+
used = max(used, L)
|
|
93
|
+
ok &= good
|
|
94
|
+
trace = (g[:, :, 0] + 2 * g[:, :, 1]).sum(axis=1).real
|
|
95
|
+
integrand[:, i] = x**3 * n[shells].real * polarizability(x) * trace
|
|
96
|
+
energy = -HBAR / (3 * np.pi * C_NM**3) * integrand @ w
|
|
97
|
+
return CasimirPolder(r=r, energy=energy, xi=xi, integrand=integrand, orders=used, converged=ok)
|
pystratify/chiral.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Multilayered spheres with chiral (Pasteur) layers.
|
|
2
|
+
|
|
3
|
+
Constitutive relations (Gaussian units, time dependence exp(-i omega t)):
|
|
4
|
+
|
|
5
|
+
D = eps E + i kappa H, B = mu H - i kappa E,
|
|
6
|
+
|
|
7
|
+
the convention of treams (Beutel et al., Comput. Phys. Commun. 297, 109076
|
|
8
|
+
(2024)) and of Lindell et al., *Electromagnetic Waves in Chiral and
|
|
9
|
+
Bi-Isotropic Media* (1994) after exp(jwt) -> exp(-iwt). In every layer the
|
|
10
|
+
field splits into the Beltrami fields Q_s = E + s i Z H (s = +-1,
|
|
11
|
+
Z = sqrt(mu/eps) = mu/n), which obey curl Q_s = s k_s Q_s with
|
|
12
|
+
k_s = k0 (n + s kappa): kappa > 0 slows helicity +1 (left-circular in the
|
|
13
|
+
optics convention). The host must be achiral.
|
|
14
|
+
|
|
15
|
+
Per multipole order the fields of a layer are W_s = M + s N with wavenumber
|
|
16
|
+
k_s, and E_tan, H_tan continuous at an interface gives, with channel values
|
|
17
|
+
v_s = u_s/x_s and derivatives d_s = u_s'/x_s (u = A psi + B xi),
|
|
18
|
+
|
|
19
|
+
v' = C_v v, d' = C_d d, C_v = [[1+z, 1-z], [1-z, 1+z]]/2,
|
|
20
|
+
C_d = [[1+z, z-1], [z-1, 1+z]]/2, z = Z_{j+1} / Z_j,
|
|
21
|
+
|
|
22
|
+
so helicity mixes only through the impedance contrast (a dual, impedance-
|
|
23
|
+
matched particle conserves helicity). The regular solution is swept
|
|
24
|
+
outwards as the 2x2 matrix rho = Xi R Psi^-1 (B = R A, scaled at the
|
|
25
|
+
interface as in the achiral solver): with D1 = psi'/psi, D3 = xi'/xi,
|
|
26
|
+
|
|
27
|
+
rho' = Lambda^-1 (N0 + N1 rho) (D0 + D1m rho)^-1 Lambda,
|
|
28
|
+
N0 = D1' C_v - C_d D1, N1 = D1' C_v - C_d D3,
|
|
29
|
+
D0 = C_d D1 - D3' C_v, D1m = C_d D3 - D3' C_v, Lambda = D1' - D3',
|
|
30
|
+
|
|
31
|
+
the matrix form of the achiral Moebius step. As there, the large parts
|
|
32
|
+
(l+1)/x of D1 and l/x of D3 are combined analytically, so N0 and D1m - which
|
|
33
|
+
vanish for identical media - are built from the material contrasts
|
|
34
|
+
directly, and rho is carried as element-wise logarithms so it may fall far
|
|
35
|
+
below the double range across thick absorbing shells.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
|
|
44
|
+
from .convergence import truncation_order
|
|
45
|
+
from .riccati import log_riccati
|
|
46
|
+
from .sheets import _feibelman_arrays, _sheet_arrays, _sheet_terms
|
|
47
|
+
from .solver import _batch, _side
|
|
48
|
+
|
|
49
|
+
__all__ = ["ChiralSolution", "solve_chiral", "HELICITY_TO_TMTE", "log_matmul"]
|
|
50
|
+
|
|
51
|
+
#: (TM, TE) = (N, M) amplitudes of W_+ = M + N and W_- = M - N, as columns
|
|
52
|
+
HELICITY_TO_TMTE = np.array([[1.0, -1.0], [1.0, 1.0]])
|
|
53
|
+
_TMTE_TO_HELICITY = np.linalg.inv(HELICITY_TO_TMTE)
|
|
54
|
+
_SIGN = np.array([1.0, -1.0])
|
|
55
|
+
_SAFE_LOG = 600.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _inv2(m):
|
|
59
|
+
a, b, c, d = m[..., 0, 0], m[..., 0, 1], m[..., 1, 0], m[..., 1, 1]
|
|
60
|
+
out = np.empty_like(m)
|
|
61
|
+
det = a * d - b * c
|
|
62
|
+
out[..., 0, 0], out[..., 0, 1], out[..., 1, 0], out[..., 1, 1] = d / det, -b / det, -c / det, a / det
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _log(a):
|
|
67
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
68
|
+
return np.log(a)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _log_add(log_x, log_y):
|
|
72
|
+
with np.errstate(all="ignore"):
|
|
73
|
+
m = np.maximum(log_x.real, log_y.real)
|
|
74
|
+
m = np.where(np.isfinite(m), m, 0.0)
|
|
75
|
+
return m + np.log(np.exp(log_x - m) + np.exp(log_y - m))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def log_matmul(log_a, log_b):
|
|
79
|
+
"""log(A @ B) for stacks of 2x2 matrices given as element-wise complex logarithms."""
|
|
80
|
+
return _log_add(
|
|
81
|
+
log_a[..., :, 0, None] + log_b[..., None, 0, :],
|
|
82
|
+
log_a[..., :, 1, None] + log_b[..., None, 1, :],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ChiralSolution:
|
|
88
|
+
"""T-matrix of a multilayered sphere with chiral layers, for a batch of wavelengths.
|
|
89
|
+
|
|
90
|
+
``log_t_helicity`` has shape ``(W, L, 2, 2)``: log of the host T-matrix
|
|
91
|
+
block in the helicity basis [out, in] (index 0 = +1, 1 = -1) for the
|
|
92
|
+
waves W_s = M + s N; ``t_matrix`` is the same block in the (TM, TE)
|
|
93
|
+
basis used by the far-field functions.
|
|
94
|
+
|
|
95
|
+
Internal fields, shape ``(W, N + 1, L, 2, 2)`` [layer, ..., channel, incident
|
|
96
|
+
helicity]: in layer j the regular solution excited by a unit incident wave
|
|
97
|
+
W_h in the host is sum_c (alpha_ch psi_l(k_jc r) + beta_ch xi_l(k_jc r)) W_c,
|
|
98
|
+
with ``log_alpha`` = log alpha (the identity in the host) and ``log_r`` =
|
|
99
|
+
log R, beta = R alpha (zero in the core, T in the host). The outgoing
|
|
100
|
+
solution, a unit outgoing wave W_h in the host and nothing incoming, is
|
|
101
|
+
``log_beta_out`` = log beta and ``log_s`` = log S, alpha = S beta (zero in the host).
|
|
102
|
+
|
|
103
|
+
Interface maps, shape ``(W, N, L, 2, 2)``: ``log_in[:, j]`` carries the
|
|
104
|
+
psi amplitudes of any regular solution from layer j + 1 to layer j
|
|
105
|
+
(alpha_j = M alpha_{j+1}), ``log_out[:, j]`` the xi amplitudes of any
|
|
106
|
+
outgoing solution from layer j to layer j + 1 (beta_{j+1} = M beta_j).
|
|
107
|
+
Chained, they continue a field found in one layer (e.g. of a source) to
|
|
108
|
+
the others without inverting ``log_alpha`` or ``log_beta_out``.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
radii: np.ndarray
|
|
112
|
+
n: np.ndarray # (W, N + 1)
|
|
113
|
+
mu: np.ndarray # (W, N + 1)
|
|
114
|
+
kappa: np.ndarray # (W, N + 1)
|
|
115
|
+
wavelength: np.ndarray # (W,)
|
|
116
|
+
orders: np.ndarray # (L,)
|
|
117
|
+
log_t_helicity: np.ndarray # (W, L, 2, 2)
|
|
118
|
+
log_alpha: np.ndarray # (W, N + 1, L, 2, 2)
|
|
119
|
+
log_r: np.ndarray # (W, N + 1, L, 2, 2)
|
|
120
|
+
log_beta_out: np.ndarray # (W, N + 1, L, 2, 2)
|
|
121
|
+
log_s: np.ndarray # (W, N + 1, L, 2, 2)
|
|
122
|
+
log_in: np.ndarray # (W, N, L, 2, 2)
|
|
123
|
+
log_out: np.ndarray # (W, N, L, 2, 2)
|
|
124
|
+
sheet_sigma: np.ndarray | None = None # (W, N): 2D sheets at the interfaces (see pystratify.sheets)
|
|
125
|
+
sheet_zeta: np.ndarray | None = None # (W, N)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def has_sheets(self) -> bool:
|
|
129
|
+
return self.sheet_sigma is not None and bool(np.any(self.sheet_sigma) or np.any(self.sheet_zeta))
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def n_shells(self) -> int:
|
|
133
|
+
return self.radii.size
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def k(self) -> np.ndarray:
|
|
137
|
+
"""Mean wavenumbers k0 n, shape (W, N + 1) (the host's is exact)."""
|
|
138
|
+
return 2 * np.pi * self.n / self.wavelength[:, None]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def k_helicity(self) -> np.ndarray:
|
|
142
|
+
"""Wavenumbers k0 (n + s kappa) of helicity s = +1, -1: shape (W, N + 1, 2)."""
|
|
143
|
+
return 2 * np.pi * (self.n[..., None] + _SIGN * self.kappa[..., None]) / self.wavelength[:, None, None]
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def t_helicity(self) -> np.ndarray:
|
|
147
|
+
with np.errstate(under="ignore"):
|
|
148
|
+
return np.exp(self.log_t_helicity)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def t_matrix(self) -> np.ndarray:
|
|
152
|
+
"""T-matrix blocks (W, L, 2, 2) in the (TM, TE) basis [out, in]."""
|
|
153
|
+
return HELICITY_TO_TMTE @ self.t_helicity @ _TMTE_TO_HELICITY
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _interface_matrices(l, x_in, x_out, diff, side_in, side_out, delta):
|
|
157
|
+
"""N0, N1, D0, D1m (W, L, 2, 2) of one interface.
|
|
158
|
+
|
|
159
|
+
``x_*``, ``diff`` = x_in - x_out (from the contrasts): (W, 2); ``side_*`` =
|
|
160
|
+
(r, X) = (psi_{l+1}/psi_l, xi_{l-1}/xi_l), each (W, 2, L); ``delta`` = 1 - z.
|
|
161
|
+
"""
|
|
162
|
+
(r_i, big_i), (r_o, big_o) = side_in, side_out
|
|
163
|
+
xi, xo, diff = x_in[..., None], x_out[..., None], diff[..., None] # (W, 2, 1)
|
|
164
|
+
h = (1 - delta / 2)[:, None, None]
|
|
165
|
+
half = (delta / 2)[:, None, None]
|
|
166
|
+
shape = r_i.shape[:1] + (l.size, 2, 2)
|
|
167
|
+
n0, n1, d0, d1 = (np.empty(shape, dtype=complex) for _ in range(4))
|
|
168
|
+
|
|
169
|
+
def put(target, c, e, value):
|
|
170
|
+
target[..., c, e] = value
|
|
171
|
+
|
|
172
|
+
for c in (0, 1):
|
|
173
|
+
e = 1 - c
|
|
174
|
+
# diagonal: the large parts combine into contrasts
|
|
175
|
+
put(n0, c, c, h[:, 0] * ((l + 1) * diff[:, c] / (xi[:, c] * xo[:, c]) + r_i[:, c] - r_o[:, c]))
|
|
176
|
+
put(n1, c, c, h[:, 0] * ((l + 1) / xo[:, c] - r_o[:, c] - big_i[:, c] + l / xi[:, c]))
|
|
177
|
+
put(d0, c, c, h[:, 0] * ((l + 1) / xi[:, c] - r_i[:, c] - big_o[:, c] + l / xo[:, c]))
|
|
178
|
+
put(d1, c, c, h[:, 0] * (big_i[:, c] - big_o[:, c] + l * diff[:, c] / (xi[:, c] * xo[:, c])))
|
|
179
|
+
# off-diagonal: proportional to the impedance contrast
|
|
180
|
+
put(n0, c, e, half[:, 0] * ((l + 1) * (1 / xo[:, c] + 1 / xi[:, e]) - r_o[:, c] - r_i[:, e]))
|
|
181
|
+
put(n1, c, e, half[:, 0] * ((l + 1) / xo[:, c] - r_o[:, c] + big_i[:, e] - l / xi[:, e]))
|
|
182
|
+
put(d0, c, e, -half[:, 0] * ((l + 1) / xi[:, e] - r_i[:, e] + big_o[:, c] - l / xo[:, c]))
|
|
183
|
+
put(d1, c, e, -half[:, 0] * (big_i[:, e] + big_o[:, c] - l / xi[:, e] - l / xo[:, c]))
|
|
184
|
+
return n0, n1, d0, d1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _sheet_corrections(d1_in, d3_in, d1_out, d3_out, z_in, z_out, sigma, a, tau, p):
|
|
188
|
+
"""Additions to N0, N1, D0, D1m (W, L, 2, 2) from a 2D sheet on the interface (pystratify.sheets).
|
|
189
|
+
|
|
190
|
+
With the sheet, v' = C_v^ v + S_v d and d' = C_d^ d + S_d v, where (J = [[1, -1], [-1, 1]],
|
|
191
|
+
O = [[1, 1], [1, 1]], g = tau / p): C_v^ = C_v + z (tau - 1) J / 2, C_d^ = C_d + (tau - 1) J / 2,
|
|
192
|
+
S_v = g i Z' sigma J / 2, S_d = -i Z' sigma O / 2 - g a J / (2 i Z). ``d*``: (W, L, 2) per
|
|
193
|
+
channel; ``z_*``, ``sigma``: (W,); ``a``, ``tau``, ``p``: (W, L).
|
|
194
|
+
"""
|
|
195
|
+
J = np.array([[1.0, -1.0], [-1.0, 1.0]])
|
|
196
|
+
O = np.ones((2, 2))
|
|
197
|
+
zeta = (z_out / z_in)[:, None, None, None]
|
|
198
|
+
tau, a, gain = tau[..., None, None], a[..., None, None], (tau / p)[..., None, None]
|
|
199
|
+
sig = sigma[:, None, None, None]
|
|
200
|
+
dcv = zeta * (tau - 1) / 2 * J
|
|
201
|
+
dcd = (tau - 1) / 2 * J
|
|
202
|
+
s_v = gain * 1j * z_out[:, None, None, None] * sig / 2 * J
|
|
203
|
+
s_d = -1j * z_out[:, None, None, None] * sig / 2 * O - gain * a / (2j * z_in[:, None, None, None]) * J
|
|
204
|
+
|
|
205
|
+
def left(d, m): # diag(d) @ m
|
|
206
|
+
return d[..., :, None] * m
|
|
207
|
+
|
|
208
|
+
def right(m, d): # m @ diag(d)
|
|
209
|
+
return m * d[..., None, :]
|
|
210
|
+
|
|
211
|
+
n0 = left(d1_out, dcv) - right(dcd, d1_in) + right(left(d1_out, s_v), d1_in) - s_d
|
|
212
|
+
n1 = left(d1_out, dcv) - right(dcd, d3_in) + right(left(d1_out, s_v), d3_in) - s_d
|
|
213
|
+
d0 = right(dcd, d1_in) - left(d3_out, dcv) + s_d - right(left(d3_out, s_v), d1_in)
|
|
214
|
+
d1 = right(dcd, d3_in) - left(d3_out, dcv) + s_d - right(left(d3_out, s_v), d3_in)
|
|
215
|
+
return n0, n1, d0, d1
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _log_moebius(n0, n1, d0, d1, log_rho):
|
|
219
|
+
"""log[(N0 + N1 rho)(D0 + D1m rho)^-1] element-wise, rho = exp(log_rho), for any magnitude of rho,
|
|
220
|
+
and log[(D0 + D1m rho)^-1] (the inverse amplitude transfer up to Lambda).
|
|
221
|
+
|
|
222
|
+
Where every element of rho is below e^-600, rho = e^s rho_hat and the result
|
|
223
|
+
is N0 D0^-1 + e^s N1 rho_hat D0^-1: the neglected terms are e^-600 relative to
|
|
224
|
+
one of these two (the N0 D0^-1 D1m rho D0^-1 cross term matters only if N0 is
|
|
225
|
+
itself below e^-600). Above e^600, rho is scaled down to e^600, which changes
|
|
226
|
+
the ratio only by e^-600 relative.
|
|
227
|
+
"""
|
|
228
|
+
with np.errstate(invalid="ignore"):
|
|
229
|
+
s = np.max(log_rho.real, axis=(-2, -1)) # (W, L)
|
|
230
|
+
tiny = np.isfinite(s) & (s <= -_SAFE_LOG)
|
|
231
|
+
shift = np.where(np.isfinite(s) & (s > _SAFE_LOG), s - _SAFE_LOG, 0.0)[..., None, None]
|
|
232
|
+
with np.errstate(all="ignore"):
|
|
233
|
+
rho = np.exp(np.where(tiny[..., None, None], -np.inf, log_rho - shift))
|
|
234
|
+
den_inv = _inv2(d0 + d1 @ rho)
|
|
235
|
+
out = _log((n0 + n1 @ rho) @ den_inv)
|
|
236
|
+
log_den_inv = _log(den_inv) - shift
|
|
237
|
+
if tiny.any():
|
|
238
|
+
rho_hat = np.exp(log_rho[tiny] - s[tiny][:, None, None])
|
|
239
|
+
inv = _inv2(d0[tiny])
|
|
240
|
+
out[tiny] = _log_add(_log(n0[tiny] @ inv), s[tiny][:, None, None] + _log(n1[tiny] @ rho_hat @ inv))
|
|
241
|
+
return out, log_den_inv
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _log_moebius_inward(n0, n1, d0, d1, log_sig):
|
|
245
|
+
"""Outgoing solution across an interface, outer to inner:
|
|
246
|
+
log[(sig N0 - D0)^-1 (D1m - sig N1)] element-wise for sig = exp(log_sig) (already
|
|
247
|
+
similarity-transformed by Lambda), any magnitude; below e^-600 to first order in sig."""
|
|
248
|
+
with np.errstate(invalid="ignore"):
|
|
249
|
+
s = np.max(log_sig.real, axis=(-2, -1))
|
|
250
|
+
tiny = np.isfinite(s) & (s <= -_SAFE_LOG)
|
|
251
|
+
shift = np.where(np.isfinite(s) & (s > _SAFE_LOG), s - _SAFE_LOG, 0.0)[..., None, None]
|
|
252
|
+
with np.errstate(all="ignore"):
|
|
253
|
+
sig = np.exp(np.where(tiny[..., None, None], -np.inf, log_sig - shift))
|
|
254
|
+
out = _log(_inv2(sig @ n0 - d0) @ (d1 - sig @ n1))
|
|
255
|
+
if tiny.any():
|
|
256
|
+
sig_hat = np.exp(log_sig[tiny] - s[tiny][:, None, None])
|
|
257
|
+
inv = _inv2(d0[tiny])
|
|
258
|
+
first = inv @ sig_hat @ (n1[tiny] - n0[tiny] @ inv @ d1[tiny])
|
|
259
|
+
out[tiny] = _log_add(_log(-inv @ d1[tiny]), s[tiny][:, None, None] + _log(first))
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def solve_chiral(radii, n, kappa, wavelength, mu=None, l_max=None, sheets=None) -> ChiralSolution:
|
|
264
|
+
"""Solve a multilayered sphere whose shells may be chiral.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
radii : (N,) outer radii of the core and shells, strictly increasing.
|
|
269
|
+
n : (N + 1,) or (W, N + 1) refractive indices n = sqrt(eps mu), host last.
|
|
270
|
+
kappa : like ``n``, Pasteur chirality parameters (dimensionless, complex
|
|
271
|
+
for a lossy chiral response); the host's must be 0, and n +- kappa must
|
|
272
|
+
not be zero or negative real.
|
|
273
|
+
wavelength : scalar or (W,) vacuum wavelength(s), same unit as ``radii``.
|
|
274
|
+
mu : like ``n``, relative permeabilities (default 1).
|
|
275
|
+
l_max : truncation order; default :func:`truncation_order` (Wiscombe) for
|
|
276
|
+
the shortest wavelength, as :func:`~pystratify.solve`.
|
|
277
|
+
sheets : 2D materials at interfaces, ``{j: Sheet(...)}`` or ``{j: conductivity}``
|
|
278
|
+
(j = 0: surface of the core), as :func:`~pystratify.solve`; see :mod:`pystratify.sheets`.
|
|
279
|
+
|
|
280
|
+
With ``kappa = 0`` everywhere the result equals :func:`~pystratify.solve`'s
|
|
281
|
+
(``t_matrix`` then is diagonal).
|
|
282
|
+
"""
|
|
283
|
+
radii = np.atleast_1d(np.asarray(radii, dtype=float))
|
|
284
|
+
if radii.ndim != 1 or radii.size == 0 or not np.all(np.isfinite(radii)):
|
|
285
|
+
raise ValueError("radii must be a non-empty 1-D array of finite values")
|
|
286
|
+
if radii[0] <= 0 or np.any(np.diff(radii) <= 0):
|
|
287
|
+
raise ValueError("radii must be positive and strictly increasing")
|
|
288
|
+
N = radii.size
|
|
289
|
+
n, mu, wavelength = _batch(n, mu, wavelength, N + 1)
|
|
290
|
+
try:
|
|
291
|
+
kappa = np.ascontiguousarray(np.broadcast_to(np.asarray(kappa, dtype=complex), n.shape))
|
|
292
|
+
except ValueError:
|
|
293
|
+
raise ValueError(f"kappa needs shape ({N + 1},) or ({wavelength.size}, {N + 1}), host last") from None
|
|
294
|
+
if not np.all(np.isfinite(kappa)):
|
|
295
|
+
raise ValueError("chirality parameters must be finite")
|
|
296
|
+
if np.any(kappa[:, -1] != 0):
|
|
297
|
+
raise ValueError("the host must be achiral (kappa = 0 in the last entry)")
|
|
298
|
+
helicity_n = n[..., None] + _SIGN * kappa[..., None]
|
|
299
|
+
if np.any(helicity_n == 0) or np.any((helicity_n.imag == 0) & (helicity_n.real < 0)):
|
|
300
|
+
raise ValueError(
|
|
301
|
+
"n +- kappa must not be zero or negative real (a lossless backward-wave helicity is not supported)"
|
|
302
|
+
)
|
|
303
|
+
if l_max is None:
|
|
304
|
+
l_max = max(truncation_order(radii[-1], abs(v), lam) for v, lam in zip(n[:, -1], wavelength))
|
|
305
|
+
l_max = int(l_max)
|
|
306
|
+
if l_max < 1:
|
|
307
|
+
raise ValueError("l_max must be >= 1")
|
|
308
|
+
l = np.arange(1, l_max + 1)
|
|
309
|
+
W = wavelength.size
|
|
310
|
+
|
|
311
|
+
k0 = 2 * np.pi / wavelength
|
|
312
|
+
kc = k0[:, None, None] * helicity_n # (W, N + 1, 2)
|
|
313
|
+
x_in = kc[:, :N] * radii[:, None] # (W, N, 2): k_{j,s} R_j
|
|
314
|
+
x_out = kc[:, 1:] * radii[:, None] # (W, N, 2): k_{j+1,s} R_j
|
|
315
|
+
lp_in, lx_in, r_in, big_in, d1_in, d3_in = _side(x_in, *log_riccati(x_in, l_max + 1), l)
|
|
316
|
+
lp_out, lx_out, r_out, big_out, d1_out, d3_out = _side(x_out, *log_riccati(x_out, l_max + 1), l)
|
|
317
|
+
# 1 - Z_{j+1}/Z_j from the contrasts, Z = mu/n
|
|
318
|
+
n_i, n_o, mu_i, mu_o = n[:, :N], n[:, 1:], mu[:, :N], mu[:, 1:]
|
|
319
|
+
delta = (mu_i * (n_o - n_i) + n_i * (mu_i - mu_o)) / (mu_i * n_o) # (W, N)
|
|
320
|
+
# x - x' per helicity from the contrasts, not by subtraction of the products
|
|
321
|
+
diff_n = (n_i - n_o)[..., None] + _SIGN * (kappa[:, :N] - kappa[:, 1:])[..., None] # (W, N, 2)
|
|
322
|
+
|
|
323
|
+
if _feibelman_arrays(sheets, N, W) is not None:
|
|
324
|
+
raise NotImplementedError("d-parameters (Feibelman) are not part of the chiral solver: use solve()")
|
|
325
|
+
sigma, zeta = _sheet_arrays(sheets, N, W)
|
|
326
|
+
sheet_a, sheet_p, sheet_tau = _sheet_terms(sigma, zeta, k0, radii, l)
|
|
327
|
+
|
|
328
|
+
log_rho = np.full((W, l_max, 2, 2), -np.inf, dtype=complex)
|
|
329
|
+
log_rho_out = np.empty((N, W, l_max, 2, 2), dtype=complex)
|
|
330
|
+
log_transfer_inv = np.empty((N, W, l_max, 2, 2), dtype=complex) # a = K^-1 a' at interface j
|
|
331
|
+
interface = []
|
|
332
|
+
for j in range(N):
|
|
333
|
+
if j: # carry rho across shell j: rho_ce *= [xi_c(o)/xi_c(i)] [psi_e(i)/psi_e(o)]
|
|
334
|
+
grow_xi = np.moveaxis(lx_in[:, j] - lx_out[:, j - 1], 1, -1) # (W, L, 2)
|
|
335
|
+
fall_psi = np.moveaxis(lp_out[:, j - 1] - lp_in[:, j], 1, -1)
|
|
336
|
+
log_rho = log_rho + grow_xi[..., :, None] + fall_psi[..., None, :]
|
|
337
|
+
mats = _interface_matrices(
|
|
338
|
+
l,
|
|
339
|
+
x_in[:, j],
|
|
340
|
+
x_out[:, j],
|
|
341
|
+
k0[:, None] * radii[j] * diff_n[:, j],
|
|
342
|
+
(r_in[:, j], big_in[:, j]),
|
|
343
|
+
(r_out[:, j], big_out[:, j]),
|
|
344
|
+
delta[:, j],
|
|
345
|
+
)
|
|
346
|
+
if np.any(sigma[:, j]) or np.any(zeta[:, j]):
|
|
347
|
+
extra = _sheet_corrections(
|
|
348
|
+
*(np.moveaxis(v[:, j], 1, -1) for v in (d1_in, d3_in, d1_out, d3_out)),
|
|
349
|
+
mu[:, j] / n[:, j],
|
|
350
|
+
mu[:, j + 1] / n[:, j + 1],
|
|
351
|
+
sigma[:, j],
|
|
352
|
+
sheet_a[:, j],
|
|
353
|
+
sheet_tau[:, j],
|
|
354
|
+
sheet_p[:, j],
|
|
355
|
+
)
|
|
356
|
+
mats = tuple(m + e for m, e in zip(mats, extra))
|
|
357
|
+
interface.append(mats)
|
|
358
|
+
log_m, log_den_inv = _log_moebius(*mats, log_rho)
|
|
359
|
+
# similarity with Lambda = diag(-i / (psi xi)) at the outer arguments
|
|
360
|
+
lam = np.moveaxis(lp_out[:, j] + lx_out[:, j], 1, -1) # (W, L, 2): log(psi xi)
|
|
361
|
+
log_rho = log_m + lam[..., :, None] - lam[..., None, :]
|
|
362
|
+
log_rho_out[j] = log_rho
|
|
363
|
+
log_transfer_inv[j] = log_den_inv + (np.log(-1j) - lam)[..., None, :] # (D0 + D1m rho)^-1 Lambda
|
|
364
|
+
|
|
365
|
+
# amplitudes from the host inwards: scaled a = diag(psi/x) alpha, a_inner = K^-1 a_outer
|
|
366
|
+
log_x_in, log_x_out = np.log(x_in), np.log(x_out) # (W, N, 2)
|
|
367
|
+
log_alpha = np.empty((W, N + 1, l_max, 2, 2), dtype=complex)
|
|
368
|
+
log_r = np.full((W, N + 1, l_max, 2, 2), -np.inf, dtype=complex)
|
|
369
|
+
log_in = np.empty((W, N, l_max, 2, 2), dtype=complex)
|
|
370
|
+
log_alpha[:, N] = _log(np.eye(2) + 0j)
|
|
371
|
+
for j in range(N - 1, -1, -1):
|
|
372
|
+
outer = np.moveaxis(lp_out[:, j], 1, -1) - log_x_out[:, j, None, :] # (W, L, 2): log(psi/x)
|
|
373
|
+
inner = np.moveaxis(lp_in[:, j], 1, -1) - log_x_in[:, j, None, :]
|
|
374
|
+
# alpha_j = diag(x/psi)_in K^-1 diag(psi/x)_out alpha_{j+1}
|
|
375
|
+
log_in[:, j] = log_transfer_inv[j] + outer[..., None, :] - inner[..., :, None]
|
|
376
|
+
scaled = log_matmul(log_transfer_inv[j], log_alpha[:, j + 1] + outer[..., :, None])
|
|
377
|
+
log_alpha[:, j] = scaled - inner[..., :, None]
|
|
378
|
+
# R of layer j + 1 from rho at its inner boundary: R_ce = rho_ce (x_c / xi_c) (psi_e / x_e)
|
|
379
|
+
to_r_rows = log_x_out[:, j, None, :] - np.moveaxis(lx_out[:, j], 1, -1)
|
|
380
|
+
log_r[:, j + 1] = log_rho_out[j] + to_r_rows[..., :, None] + outer[..., None, :]
|
|
381
|
+
# outgoing solution from the host inwards: scaled b = diag(xi/x) beta, sigma = (psi/x) S (x/xi),
|
|
382
|
+
# sigma = (sig' N0 - D0)^-1 (D1m - sig' N1) and b = (N0 sigma + N1)^-1 Lambda b'
|
|
383
|
+
log_beta_out = np.empty((W, N + 1, l_max, 2, 2), dtype=complex)
|
|
384
|
+
log_s = np.full((W, N + 1, l_max, 2, 2), -np.inf, dtype=complex)
|
|
385
|
+
log_out = np.empty((W, N, l_max, 2, 2), dtype=complex)
|
|
386
|
+
log_beta_out[:, N] = _log(np.eye(2) + 0j)
|
|
387
|
+
log_sig = np.full((W, l_max, 2, 2), -np.inf, dtype=complex) # outer side of interface N - 1
|
|
388
|
+
for j in range(N - 1, -1, -1):
|
|
389
|
+
n0, n1, d0, d1 = interface[j]
|
|
390
|
+
lam = np.moveaxis(lp_out[:, j] + lx_out[:, j], 1, -1)
|
|
391
|
+
# sig~' = Lambda sig' Lambda^-1, Lambda = diag(-i / (psi xi)): element factor (psi xi)_e / (psi xi)_c
|
|
392
|
+
log_sig_in = _log_moebius_inward(n0, n1, d0, d1, log_sig - lam[..., :, None] + lam[..., None, :])
|
|
393
|
+
with np.errstate(under="ignore", over="ignore"):
|
|
394
|
+
forward = n0 @ np.exp(log_sig_in) + n1
|
|
395
|
+
transfer = _inv2(forward) # (N0 sigma + N1)^-1, then Lambda
|
|
396
|
+
outer_xi = np.moveaxis(lx_out[:, j], 1, -1) - log_x_out[:, j, None, :] # log(xi/x), outer side
|
|
397
|
+
inner_xi = np.moveaxis(lx_in[:, j], 1, -1) - log_x_in[:, j, None, :]
|
|
398
|
+
# beta_{j+1} = diag(x/xi)_out Lambda^-1 (N0 sigma + N1) diag(xi/x)_in beta_j, Lambda^-1 = diag(i psi xi)
|
|
399
|
+
log_out[:, j] = _log(forward) + (np.log(1j) + lam - outer_xi)[..., :, None] + inner_xi[..., None, :]
|
|
400
|
+
log_transfer = _log(transfer) + (np.log(-1j) - lam)[..., None, :]
|
|
401
|
+
scaled = log_matmul(log_transfer, log_beta_out[:, j + 1] + outer_xi[..., :, None])
|
|
402
|
+
log_beta_out[:, j] = scaled - inner_xi[..., :, None]
|
|
403
|
+
# S of layer j from sigma at its outer boundary: S_ce = sigma_ce (x_c / psi_c) (xi_e / x_e)
|
|
404
|
+
inner_psi = np.moveaxis(lp_in[:, j], 1, -1) - log_x_in[:, j, None, :]
|
|
405
|
+
log_s[:, j] = log_sig_in - inner_psi[..., :, None] + inner_xi[..., None, :]
|
|
406
|
+
if j: # carry sigma across layer j to the outer side of interface j - 1
|
|
407
|
+
fall_psi = np.moveaxis(lp_out[:, j - 1] - lp_in[:, j], 1, -1)
|
|
408
|
+
grow_xi = np.moveaxis(lx_in[:, j] - lx_out[:, j - 1], 1, -1)
|
|
409
|
+
log_sig = log_sig_in + fall_psi[..., :, None] + grow_xi[..., None, :]
|
|
410
|
+
return ChiralSolution(
|
|
411
|
+
radii=radii,
|
|
412
|
+
n=n,
|
|
413
|
+
mu=mu,
|
|
414
|
+
kappa=kappa,
|
|
415
|
+
wavelength=wavelength,
|
|
416
|
+
orders=l,
|
|
417
|
+
log_t_helicity=log_r[:, N],
|
|
418
|
+
log_alpha=log_alpha,
|
|
419
|
+
log_r=log_r,
|
|
420
|
+
log_beta_out=log_beta_out,
|
|
421
|
+
log_s=log_s,
|
|
422
|
+
log_in=log_in,
|
|
423
|
+
log_out=log_out,
|
|
424
|
+
sheet_sigma=sigma,
|
|
425
|
+
sheet_zeta=zeta,
|
|
426
|
+
)
|