treams_core/pw/
polarization.rs

1//! Plane-wave polarization vectors, their wavevector derivatives and the normal wavenumber.
2//!
3//! Upstream: `treams.special.vpw_M`, `vpw_N` and `vpw_A`, and `treams.misc.wave_vec_z`.
4#![allow(clippy::indexing_slicing)] // Fixed three-component vectors and jet slots.
5
6use crate::{
7    Complex, Error, Result,
8    numerics::{Jet, complex_sqrt, finite, ratio},
9    special::{check_pol, polarized_wave},
10};
11
12/// The algebraic norm `sqrt(Σ v^2)` (principal root) of complex components.
13///
14/// Nearly axial directions may have transverse components small enough that their
15/// squares underflow while their azimuth matters, so extreme magnitudes are scaled
16/// before squaring.
17#[inline]
18fn algebraic_norm(values: &[Complex]) -> Complex {
19    let scale = values
20        .iter()
21        .map(|v| v.re.abs().max(v.im.abs()))
22        .fold(0.0, f64::max);
23    if scale == 0.0 {
24        return Complex::default();
25    }
26    // Squares of magnitudes within 1e±150 stay well inside the normal f64 range
27    // (1e±308) and need no scaling. Outside it, scaling keeps tiny near-axis
28    // components from underflowing to zero and huge ones from overflowing.
29    if (1e-150..=1e150).contains(&scale) {
30        return complex_sqrt(values.iter().map(|v| v * v).sum::<Complex>());
31    }
32    complex_sqrt(values.iter().map(|v| (v / scale).powu(2)).sum::<Complex>()) * scale
33}
34/// The transverse norm `sqrt(kx^2 + ky^2)` (principal root) and the unit transverse
35/// direction `(kx, ky) / norm`, which is `(cos phi, sin phi)` for a real wavevector.
36///
37/// Gives zeros for `kx = ky = 0`, and an error when a complex `(kx, ky)` is nonzero
38/// but has a zero algebraic norm, where the direction is undefined.
39#[inline]
40pub(crate) fn transverse_values(vector: [Complex; 2]) -> Result<(Complex, [Complex; 2])> {
41    let scale = vector
42        .iter()
43        .map(|v| v.re.abs().max(v.im.abs()))
44        .fold(0.0, f64::max);
45    Ok(if scale == 0.0 {
46        (Complex::default(), [Complex::default(); 2])
47    } else {
48        let (scaled, factor) = if (1e-150..=1e150).contains(&scale) {
49            ([vector[0], vector[1]], 1.0)
50        } else {
51            ([vector[0] / scale, vector[1] / scale], scale)
52        };
53        let norm = complex_sqrt(scaled[0] * scaled[0] + scaled[1] * scaled[1]);
54        if norm == Complex::default() {
55            return Err(Error::InvalidInput(
56                "undefined polarization for a null transverse vector".into(),
57            ));
58        }
59        (norm * factor, scaled.map(|v| ratio(v, norm)))
60    })
61}
62/// The wavenumber `k`, the transverse norm and the unit transverse direction of a
63/// finite wavevector with a nonzero algebraic norm.
64pub(crate) fn wavenumbers(vector: [Complex; 3]) -> Result<(Complex, Complex, [Complex; 2])> {
65    if vector.iter().any(|&v| !finite(v)) {
66        return Err(Error::InvalidInput("wavevector must be finite".into()));
67    }
68    let (transverse, xy) = transverse_values([vector[0], vector[1]])?;
69    let k = algebraic_norm(&vector);
70    if k == Complex::default() || !finite(k) {
71        return Err(Error::InvalidInput(
72            "wavevector must have nonzero algebraic norm".into(),
73        ));
74    }
75    Ok((k, transverse, xy))
76}
77
78/// The direction of a wavevector `(kx, ky, kz)` as jets in its components: slots 0, 1
79/// and 2 hold the derivatives with respect to `kx`, `ky` and `kz` (only the first `N`
80/// are kept).
81pub(crate) struct Direction<const N: usize> {
82    /// The wavenumber `k = sqrt(kx^2 + ky^2 + kz^2)`.
83    pub(crate) k: Jet<N>,
84    /// The transverse norm `sqrt(kx^2 + ky^2)`, which is `k sin(theta)`.
85    pub(crate) transverse: Jet<N>,
86    /// The unit transverse direction `(kx, ky) / transverse`, which is
87    /// `(cos phi, sin phi)`; zero on the z axis.
88    pub(crate) xy: [Jet<N>; 2],
89}
90impl<const N: usize> Direction<N> {
91    /// The direction of `vector`; see [`wavenumbers`] for the errors.
92    pub(crate) fn new(vector: [Complex; 3]) -> Result<Self> {
93        let (k, transverse, xy) = wavenumbers(vector)?;
94        Self::from_parts(vector, k, transverse, xy)
95    }
96    /// The direction from the values [`wavenumbers`] returns. On the z axis the
97    /// transverse direction has no derivative, so `N > 0` gives an error there.
98    #[inline]
99    pub(crate) fn from_parts(
100        vector: [Complex; 3],
101        k: Complex,
102        transverse: Complex,
103        xy: [Complex; 2],
104    ) -> Result<Self> {
105        if N != 0 && transverse == Complex::default() {
106            return Err(Error::InvalidInput("plane-wave direction derivative is undefined on the polarization axis; fix the wavevectors".into()));
107        }
108        let k_jet = Jet {
109            value: k,
110            derivative: std::array::from_fn(|a| ratio(vector[a], k)),
111        };
112        let transverse_jet = Jet {
113            value: transverse,
114            derivative: std::array::from_fn(|a| if a < 2 { xy[a] } else { Complex::default() }),
115        };
116        let xy: [Jet<N>; 2] = std::array::from_fn(|a| Jet {
117            value: xy[a],
118            derivative: std::array::from_fn(|b| {
119                if b < 2 {
120                    ratio(
121                        Complex::new(if a == b { 1.0 } else { 0.0 }, 0.0) - xy[a] * xy[b],
122                        transverse,
123                    )
124                } else {
125                    Complex::default()
126                }
127            }),
128        });
129        Ok(Self {
130            k: k_jet,
131            transverse: transverse_jet,
132            xy,
133        })
134    }
135}
136
137/// The polarization vector of a plane wave with wavevector `vector`, as jets in the
138/// wavevector components.
139///
140/// M is `i (ky, -kx, 0) / transverse` and N is `(-kx kz, -ky kz, transverse^2) /
141/// (k transverse)`, as upstream `vpw_M` and `vpw_N`; helicity combines them as
142/// `polarized_wave` does. On the z axis M is `(0, -i, 0)` and N is
143/// `(-kz / sqrt(kz^2), 0, 0)`, using the same principal norm as off the axis.
144#[inline]
145pub(crate) fn polarization_jet<const N: usize>(
146    vector: [Complex; 3],
147    pol: u8,
148    helicity: bool,
149) -> Result<[Jet<N>; 3]> {
150    check_pol(pol)?;
151    let Direction { k, transverse, xy } = Direction::new(vector)?;
152    let z = vector[2];
153    let m = if transverse.value == Complex::default() {
154        [Jet::default(), Jet::constant(-Complex::i()), Jet::default()]
155    } else {
156        [Complex::i() * xy[1], -Complex::i() * xy[0], Jet::default()]
157    };
158    // Parity M (pol 0) needs no N part.
159    if !helicity && pol == 0 {
160        return Ok(m);
161    }
162    let n = if transverse.value == Complex::default() {
163        [
164            Jet::constant(-ratio(z, k.value)),
165            Jet::default(),
166            Jet::default(),
167        ]
168    } else {
169        let longitudinal = Jet::variable(z, 2) / k;
170        [-xy[0] * longitudinal, -xy[1] * longitudinal, transverse / k]
171    };
172    Ok(polarized_wave(m, n, pol, helicity))
173}
174
175/// Plane-wave electric vector at the origin in treams normalization.
176///
177/// Upstream: `treams.special.vpw_M` (`pol` 0) and `vpw_N` (`pol` 1) at the origin with
178/// `helicity` false, and `vpw_A` with `helicity` true.
179#[inline]
180pub fn polarization(vector: [Complex; 3], pol: u8, helicity: bool) -> Result<[Complex; 3]> {
181    Ok(polarization_jet::<0>(vector, pol, helicity)?.map(|p| p.value))
182}
183
184/// The field `polarization exp(i k·position)` of a plane wave from its polarization
185/// vector, which [`polarization`] gives.
186///
187/// Upstream: `treams.special.vpw_M`, `vpw_N` and `vpw_A` at `position`.
188#[inline]
189pub fn field_value(
190    polarization: [Complex; 3],
191    k: [Complex; 3],
192    position: [Complex; 3],
193) -> Result<[Complex; 3]> {
194    if position.iter().any(|&v| !finite(v)) {
195        return Err(Error::InvalidInput("field position must be finite".into()));
196    }
197    let phase = (Complex::i() * (0..3).map(|a| k[a] * position[a]).sum::<Complex>()).exp();
198    let values = polarization.map(|p| p * phase);
199    if values.iter().any(|&v| !finite(v)) {
200        return Err(Error::NonFinite("non-finite plane-wave field".into()));
201    }
202    Ok(values)
203}
204
205/// Chain the local Cartesian polarization derivative into solver parameters.
206pub(crate) fn polarization_from_inputs<const N: usize>(
207    vector: [Jet<N>; 3],
208    pol: u8,
209) -> Result<[Jet<N>; 3]> {
210    let values = vector.map(|k| k.value);
211    if N == 0
212        || (values[0] == Complex::default()
213            && values[1] == Complex::default()
214            && vector[..2]
215                .iter()
216                .flat_map(|k| k.derivative)
217                .all(|d| d == Complex::default()))
218    {
219        return Ok(polarization(values, pol, true)?.map(Jet::constant));
220    }
221    Ok(polarization_jet::<3>(values, pol, true)?.map(|e| e.compose(vector)))
222}
223
224/// The normal wavenumber `kz = sqrt(k^2 - kx^2 - ky^2)` on the root with a nonnegative
225/// imaginary part, so the wave propagates or decays toward `+z`; zero at a cutoff.
226///
227/// Upstream: `treams.misc.wave_vec_z`.
228#[must_use]
229pub fn wave_vector_z(kx: Complex, ky: Complex, k: Complex) -> Complex {
230    let root = complex_sqrt(k * k - kx * kx - ky * ky);
231    if root.im < 0.0 { -root } else { root }
232}