treams_core/smatrix/
chirality.rs

1//! Chirality-density forms of plane-wave modes, averaged over an interval.
2//!
3//! Two kernels compute these forms from different inputs, and each differentiates its
4//! own. [`chirality_density`] takes the full and normal wavenumbers of modes with real
5//! transverse wavevectors in the xy plane and returns the forms without the helicity
6//! sign. [`oriented_chirality`] takes the real transverse components and the normal
7//! wavenumber along any Cartesian axis and includes the helicity sign of each mode.
8//!
9//! Both kernels exist because their gradients answer different questions: the first
10//! differentiates with respect to the medium (`k`, `kz`), the second with respect to
11//! the direction of incidence (`q`, `kz`). On modes with `q.q = k^2 - kz^2` their
12//! forms agree up to the helicity sign.
13//!
14//! Upstream: `treams.chirality_density`. Its interval average decays with `Re(kz)`
15//! instead of `Im(kz)` and drops the phase of the cross form; these kernels keep both.
16
17mod saved;
18
19use nalgebra::DMatrix;
20
21use crate::{
22    Complex, Error, Result,
23    numerics::{
24        Jet, finite,
25        parallel::{PARALLEL_ITEMS, try_fill_chunks, try_map},
26    },
27    special::helicity_sign,
28};
29
30/// Jet seed offsets of the six real local parameters of one mode: the full wavenumber
31/// `k` (real, imaginary) of [`chirality_density`] or the transverse components `q` of
32/// [`oriented_chirality`] at `KS` or `Q`, the normal wavenumber (real, imaginary) at
33/// `KZS` and the interval endpoints at `INTERVAL`.
34const KS: usize = 0;
35/// See [`KS`].
36const Q: usize = 0;
37/// See [`KS`].
38const KZS: usize = 2;
39/// See [`KS`].
40const INTERVAL: usize = 4;
41
42/// What [`chirality_density`] saves for its pullback: the full and normal wavenumbers
43/// and the interval.
44#[derive(Debug)]
45pub struct ChiralityDensityResidual {
46    ks: Vec<Complex>,
47    normal: Vec<Complex>,
48    interval: [f64; 2],
49}
50
51/// Complex full/normal wavenumber and real interval-endpoint cotangents.
52#[derive(Debug)]
53pub struct ChiralityDensityGradient {
54    /// Cotangents of the full wavenumbers.
55    pub ks: Vec<Complex>,
56    /// Cotangents of the normal wavenumbers.
57    pub normal: Vec<Complex>,
58    /// Cotangents of the start and end of the averaging interval.
59    pub interval: [f64; 2],
60}
61
62/// The mean of `exp(slope z)` over `z` in `interval`, from a six-term series when the
63/// exponent changes by less than 0.1 across the interval.
64fn mean_exp<const N: usize>(slope: Jet<N>, interval: [Jet<N>; 2]) -> Jet<N> {
65    let width = slope * (interval[1] - interval[0]);
66    if width.value.norm() < 0.1 {
67        let square = (width * 0.5).powi(2);
68        let mut term = Jet::constant(1.0);
69        let mut sum = term;
70        for j in 1..=6 {
71            term *= square / f64::from(2 * j * (2 * j + 1));
72            sum += term;
73        }
74        (slope * (interval[0] + interval[1]) * 0.5).exp() * sum
75    } else {
76        ((slope * interval[1]).exp() - (slope * interval[0]).exp()) / width
77    }
78}
79
80/// The up, down and cross forms of one mode of [`chirality_density`].
81fn chirality_mode<const N: usize>(k: Complex, normal: Complex, z: [f64; 2]) -> [Jet<N>; 3] {
82    chirality_forms(
83        [Jet::variable(k.re, KS), Jet::variable(k.im, KS + 1)],
84        [
85            Jet::variable(normal.re, KZS),
86            Jet::variable(normal.im, KZS + 1),
87        ],
88        [
89            Jet::variable(z[0], INTERVAL),
90            Jet::variable(z[1], INTERVAL + 1),
91        ],
92    )
93}
94
95/// The same scaled formulas for value, coordinate derivatives and directional jets.
96fn chirality_forms<const N: usize>(
97    k: [Jet<N>; 2],
98    normal: [Jet<N>; 2],
99    z: [Jet<N>; 2],
100) -> [Jet<N>; 3] {
101    // The common scale cancels algebraically; holding it fixed also avoids taking
102    // a derivative of the normalization rather than of the physical observable.
103    let scale = k[0].value.re.hypot(k[1].value.re);
104    let [kr, ki] = k.map(|v| v / scale);
105    let [nr, ni] = normal;
106    let denominator = kr.powi(2) + ki.powi(2);
107    let same = 2.0 * (kr.powi(2) + (ni / scale).powi(2)) / denominator;
108    let cross = 2.0 * (kr.powi(2) - (nr / scale).powi(2)) / denominator;
109    [
110        same * mean_exp(-2.0 * ni, z),
111        same * mean_exp(2.0 * ni, z),
112        2.0 * cross * mean_exp(2.0 * Complex::i() * nr, z),
113    ]
114}
115
116/// Up, down and coherent cross coefficients, shape (3, modes), before polarization.
117///
118/// The density is 2 Re(E* . i Z H). The cross form X enters it as Re(d* X u), with the
119/// down and up amplitudes d and u. The modes have real transverse wavevectors in the xy
120/// plane; the gradients cover the full and normal wavenumbers.
121///
122/// Upstream: `treams.chirality_density`, with the differences of the module docs.
123pub fn chirality_density(
124    ks: Vec<Complex>,
125    normal: Vec<Complex>,
126    interval: [f64; 2],
127) -> Result<(DMatrix<Complex>, ChiralityDensityResidual)> {
128    if ks.is_empty()
129        || normal.len() != ks.len()
130        || ks.iter().any(|&k| !finite(k) || k == Complex::default())
131        || normal.iter().any(|&k| !finite(k))
132        || interval.iter().any(|z| !z.is_finite())
133    {
134        return Err(Error::InvalidInput(
135            "chirality requires matching finite wavenumbers, nonzero full k and finite interval"
136                .into(),
137        ));
138    }
139    let value = mode_forms(
140        ks.len(),
141        |j| Ok(chirality_mode::<0>(ks[j], normal[j], interval)),
142        |v| v.value,
143    )?;
144    Ok((
145        value,
146        ChiralityDensityResidual {
147            ks,
148            normal,
149            interval,
150        },
151    ))
152}
153
154/// The three forms of every mode as a (3, modes) matrix, in parallel from 1024 modes.
155fn mode_forms<const N: usize>(
156    modes: usize,
157    forms: impl Fn(usize) -> Result<[Jet<N>; 3]> + Sync,
158    component: impl Fn(Jet<N>) -> Complex + Sync,
159) -> Result<DMatrix<Complex>> {
160    let mut value = DMatrix::zeros(3, modes);
161    try_fill_chunks(
162        value.as_mut_slice(),
163        3,
164        modes >= PARALLEL_ITEMS,
165        |j, column| {
166            for (out, form) in column.iter_mut().zip(forms(j)?) {
167                *out = component(form);
168                if !finite(*out) {
169                    return Err(Error::NonFinite("chirality density overflow".into()));
170                }
171            }
172            Ok(())
173        },
174    )?;
175    Ok(value)
176}
177
178/// Pair the six local real derivatives of every mode's forms with its column of the
179/// (3, modes) cotangent, in parallel from 1024 modes.
180fn mode_cotangents(
181    cotangent: &DMatrix<Complex>,
182    modes: usize,
183    forms: impl Fn(usize) -> Result<[Jet<6>; 3]> + Sync,
184) -> Result<Vec<[f64; 6]>> {
185    if cotangent.shape() != (3, modes) || cotangent.iter().any(|&g| !finite(g)) {
186        return Err(Error::InvalidInput("invalid chirality cotangent".into()));
187    }
188    try_map(modes, modes >= PARALLEL_ITEMS, |j| {
189        let forms = forms(j)?;
190        let g: [f64; 6] = std::array::from_fn(|p| {
191            forms
192                .iter()
193                .enumerate()
194                .map(|(i, c)| (cotangent[(i, j)].conj() * c.derivative[p]).re)
195                .sum()
196        });
197        if g.iter().any(|v| !v.is_finite()) {
198            return Err(Error::NonFinite("chirality derivative overflow".into()));
199        }
200        Ok(g)
201    })
202}
203
204impl ChiralityDensityResidual {
205    /// Compact output dimensions.
206    #[must_use]
207    pub fn shape(&self) -> (usize, usize) {
208        (3, self.ks.len())
209    }
210
211    /// Propagate one real direction in the complex wavenumbers and interval.
212    pub fn pushforward(
213        &self,
214        ks: &[Complex],
215        normal: &[Complex],
216        interval: [f64; 2],
217    ) -> Result<DMatrix<Complex>> {
218        if ks.len() != self.ks.len()
219            || normal.len() != self.normal.len()
220            || ks.iter().chain(normal).any(|&v| !finite(v))
221            || interval.iter().any(|v| !v.is_finite())
222        {
223            return Err(Error::InvalidInput(
224                "chirality tangents must be finite and match inputs".into(),
225            ));
226        }
227        let interval = std::array::from_fn(|j| Jet {
228            value: self.interval[j].into(),
229            derivative: [interval[j].into()],
230        });
231        mode_forms(
232            self.ks.len(),
233            |j| {
234                Ok(chirality_forms(
235                    real_direction(self.ks[j], ks[j]),
236                    real_direction(self.normal[j], normal[j]),
237                    interval,
238                ))
239            },
240            |v| v.derivative[0],
241        )
242    }
243
244    /// Recompute six local derivatives per mode instead of keeping a dense Jacobian.
245    pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<ChiralityDensityGradient> {
246        let local = mode_cotangents(cotangent, self.ks.len(), |j| {
247            Ok(chirality_mode(self.ks[j], self.normal[j], self.interval))
248        })?;
249        let mut gradient = ChiralityDensityGradient {
250            ks: Vec::with_capacity(local.len()),
251            normal: Vec::with_capacity(local.len()),
252            interval: [0.0; 2],
253        };
254        for g in local {
255            gradient.ks.push(Complex::new(g[KS], g[KS + 1]));
256            gradient.normal.push(Complex::new(g[KZS], g[KZS + 1]));
257            gradient.interval[0] += g[INTERVAL];
258            gradient.interval[1] += g[INTERVAL + 1];
259        }
260        Ok(gradient)
261    }
262}
263
264/// What [`oriented_chirality`] saves for its pullback: the real transverse components,
265/// the normal wavenumbers, the polarizations, the axis and the interval.
266#[derive(Debug)]
267pub struct OrientedChiralityResidual {
268    transverse: Vec<[f64; 2]>,
269    normal: Vec<Complex>,
270    polarizations: Vec<u8>,
271    axis: usize,
272    interval: [f64; 2],
273}
274
275/// Real transverse, complex normal and real interval-endpoint cotangents.
276#[derive(Debug)]
277pub struct OrientedChiralityGradient {
278    /// Cotangents of the two transverse components, in cyclic Cartesian order.
279    pub transverse: Vec<[f64; 2]>,
280    /// Cotangents of the normal wavenumbers.
281    pub normal: Vec<Complex>,
282    /// Cotangents of the start and end of the averaging interval.
283    pub interval: [f64; 2],
284}
285
286/// The helicity-signed up, down and cross forms of one mode of [`oriented_chirality`].
287fn oriented_chirality_mode<const N: usize>(
288    transverse: [f64; 2],
289    normal: Complex,
290    pol: u8,
291    axis: usize,
292    interval: [f64; 2],
293) -> Result<[Jet<N>; 3]> {
294    oriented_chirality_forms(
295        [
296            Jet::variable(transverse[0], Q),
297            Jet::variable(transverse[1], Q + 1),
298        ],
299        [
300            Jet::variable(normal.re, KZS),
301            Jet::variable(normal.im, KZS + 1),
302        ],
303        pol,
304        axis,
305        [
306            Jet::variable(interval[0], INTERVAL),
307            Jet::variable(interval[1], INTERVAL + 1),
308        ],
309    )
310}
311
312/// Jets of the real coordinates retain the non-holomorphic polarization inner products.
313fn oriented_chirality_forms<const N: usize>(
314    transverse: [Jet<N>; 2],
315    normal: [Jet<N>; 2],
316    pol: u8,
317    axis: usize,
318    z: [Jet<N>; 2],
319) -> Result<[Jet<N>; 3]> {
320    let [nr, ni] = normal;
321    let sign = helicity_sign(pol);
322    // The observable has a smooth limit even where the polarization gauge does not.
323    if transverse.iter().all(|q| q.value == Complex::default()) {
324        if normal.iter().all(|v| v.value == Complex::default()) {
325            return Err(Error::InvalidInput("wavevector must be nonzero".into()));
326        }
327        return Ok([
328            2.0 * sign * mean_exp(-2.0 * ni, z),
329            2.0 * sign * mean_exp(2.0 * ni, z),
330            Jet::default(),
331        ]);
332    }
333    let mut vector = [Jet::default(); 3];
334    vector[axis] = nr + Complex::i() * ni;
335    vector[(axis + 1) % 3] = transverse[0];
336    vector[(axis + 2) % 3] = transverse[1];
337    let up = crate::pw::polarization_from_inputs(vector, pol)?;
338    vector[axis] = -vector[axis];
339    let down = crate::pw::polarization_from_inputs(vector, pol)?;
340    // All six local parameters are real, so conjugation acts on their derivatives.
341    let inner = |a: [Jet<N>; 3], b: [Jet<N>; 3]| -> Jet<N> {
342        a.into_iter()
343            .zip(b)
344            .map(|(a, b)| {
345                Jet {
346                    value: a.value.conj(),
347                    derivative: a.derivative.map(|d| d.conj()),
348                } * b
349            })
350            .sum()
351    };
352    Ok([
353        2.0 * sign * inner(up, up) * mean_exp(-2.0 * ni, z),
354        2.0 * sign * inner(down, down) * mean_exp(2.0 * ni, z),
355        4.0 * sign * inner(down, up) * mean_exp(2.0 * Complex::i() * nr, z),
356    ])
357}
358
359/// Signed helicity up/down/cross forms, shape (3, modes), for any Cartesian normal.
360///
361/// The transverse components are real and follow the cyclic order after `axis`.
362/// The cross form X enters the density as Re(down* X up). The residual keeps only the
363/// geometry.
364///
365/// Upstream: `treams.chirality_density` in the helicity convention, along any axis,
366/// with the differences of the module docs.
367pub fn oriented_chirality(
368    transverse: Vec<[f64; 2]>,
369    normal: Vec<Complex>,
370    polarizations: Vec<u8>,
371    axis: usize,
372    interval: [f64; 2],
373) -> Result<(DMatrix<Complex>, OrientedChiralityResidual)> {
374    if transverse.is_empty()
375        || transverse.len() != normal.len()
376        || transverse.len() != polarizations.len()
377        || axis > 2
378        || transverse.iter().flatten().any(|q| !q.is_finite())
379        || normal.iter().any(|&k| !finite(k))
380        || polarizations.iter().any(|&p| p > 1)
381        || interval.iter().any(|z| !z.is_finite())
382    {
383        return Err(Error::InvalidInput(
384            "chirality requires matching finite geometry, polarization 0/1 and axis 0/1/2".into(),
385        ));
386    }
387    let value = mode_forms(
388        normal.len(),
389        |j| {
390            oriented_chirality_mode::<0>(transverse[j], normal[j], polarizations[j], axis, interval)
391        },
392        |v| v.value,
393    )?;
394    Ok((
395        value,
396        OrientedChiralityResidual {
397            transverse,
398            normal,
399            polarizations,
400            axis,
401            interval,
402        },
403    ))
404}
405
406impl OrientedChiralityResidual {
407    /// Compact output dimensions.
408    #[must_use]
409    pub fn shape(&self) -> (usize, usize) {
410        (3, self.normal.len())
411    }
412
413    /// Propagate transverse, normal-wavenumber and interval directions together.
414    pub fn pushforward(
415        &self,
416        transverse: &[[f64; 2]],
417        normal: &[Complex],
418        interval: [f64; 2],
419    ) -> Result<DMatrix<Complex>> {
420        if transverse.len() != self.transverse.len()
421            || normal.len() != self.normal.len()
422            || transverse.iter().flatten().any(|v| !v.is_finite())
423            || normal.iter().any(|&v| !finite(v))
424            || interval.iter().any(|v| !v.is_finite())
425        {
426            return Err(Error::InvalidInput(
427                "chirality tangents must be finite and match inputs".into(),
428            ));
429        }
430        let interval = std::array::from_fn(|j| Jet {
431            value: self.interval[j].into(),
432            derivative: [interval[j].into()],
433        });
434        mode_forms(
435            self.normal.len(),
436            |j| {
437                oriented_chirality_forms(
438                    std::array::from_fn(|a| Jet {
439                        value: self.transverse[j][a].into(),
440                        derivative: [transverse[j][a].into()],
441                    }),
442                    real_direction(self.normal[j], normal[j]),
443                    self.polarizations[j],
444                    self.axis,
445                    interval,
446                )
447            },
448            |v| v.derivative[0],
449        )
450    }
451
452    /// Recompute six local real derivatives per mode and pair them with the cotangent.
453    pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<OrientedChiralityGradient> {
454        let local = mode_cotangents(cotangent, self.normal.len(), |j| {
455            oriented_chirality_mode(
456                self.transverse[j],
457                self.normal[j],
458                self.polarizations[j],
459                self.axis,
460                self.interval,
461            )
462        })?;
463        let mut gradient = OrientedChiralityGradient {
464            transverse: Vec::with_capacity(local.len()),
465            normal: Vec::with_capacity(local.len()),
466            interval: [0.0; 2],
467        };
468        for g in local {
469            gradient.transverse.push([g[Q], g[Q + 1]]);
470            gradient.normal.push(Complex::new(g[KZS], g[KZS + 1]));
471            gradient.interval[0] += g[INTERVAL];
472            gradient.interval[1] += g[INTERVAL + 1];
473        }
474        Ok(gradient)
475    }
476}
477
478/// Real and imaginary coordinates of one complex directional input.
479fn real_direction(value: Complex, tangent: Complex) -> [Jet<1>; 2] {
480    [
481        Jet {
482            value: value.re.into(),
483            derivative: [tangent.re.into()],
484        },
485        Jet {
486            value: value.im.into(),
487            derivative: [tangent.im.into()],
488        },
489    ]
490}