treams_core/tmatrix/
sphere.rs

1//! T-matrices of multilayer chiral spheres from their Mie coefficients.
2//!
3//! Upstream: `treams.TMatrix.sphere`.
4#![allow(clippy::indexing_slicing)] // Degree indices of the 2 x 2 helicity blocks.
5
6use nalgebra::DMatrix;
7
8use crate::coeffs::{
9    LayerGradient, Material, Matrix2, MieGradient, MieResidual, mie, to_mode_order,
10};
11use crate::sw;
12use crate::{
13    Complex, Error, Result,
14    numerics::{self, finite},
15};
16
17mod saved;
18
19/// What [`sphere`] saves for its pullback: the Mie residual of each degree.
20#[derive(Clone, Debug)]
21pub struct SphereResidual {
22    k0: f64,
23    radii: Vec<f64>,
24    degrees: Vec<MieResidual>,
25}
26
27/// Gradients of the inputs of [`sphere`], in the order of its arguments.
28#[derive(Clone, Debug)]
29pub struct SphereGradient {
30    /// Gradient of the vacuum wavenumber.
31    pub k0: f64,
32    /// Gradients of the radii and the materials.
33    pub layers: LayerGradient,
34}
35
36/// The T-matrix of a concentric multilayer chiral sphere, in the order of [`sw::modes`].
37///
38/// `radii` are the boundary radii, from inner to outer, and `materials` has one more
39/// entry, the embedding medium last. Each degree `l` contributes the Mie coefficients
40/// [`mie`] at the size parameters `k0 r`, repeated on its `2 l + 1` orders.
41///
42/// Upstream: `treams.TMatrix.sphere`. Differences: the helicity convention only.
43pub fn sphere(
44    lmax: u32,
45    k0: f64,
46    radii: &[f64],
47    materials: &[Material],
48) -> Result<(DMatrix<Complex>, SphereResidual)> {
49    if !k0.is_finite() || k0 <= 0.0 {
50        return Err(Error::InvalidInput("k0 must be finite and positive".into()));
51    }
52    let dimension = sw::modes(lmax)?.len();
53    let sizes: Vec<_> = radii.iter().map(|r| k0 * r).collect();
54    let degrees = (1..=lmax)
55        .map(|l| mie(l, &sizes, materials))
56        .collect::<Result<Vec<_>>>()?;
57    let mut value = numerics::zeros(dimension, dimension)?;
58    for (block, degree) in block_degrees(degrees.len()).enumerate() {
59        value
60            .fixed_view_mut::<2, 2>(2 * block, 2 * block)
61            .copy_from(&to_mode_order(degrees[degree].value()));
62    }
63    Ok((
64        value,
65        SphereResidual {
66            k0,
67            radii: radii.to_vec(),
68            degrees,
69        },
70    ))
71}
72
73/// The degree index `l - 1` of each 2 x 2 diagonal helicity block of a T-matrix in
74/// [`sw::modes`] order with `count` degrees, which has one block per order `m` of each
75/// degree `l`.
76pub(crate) fn block_degrees(count: usize) -> impl Iterator<Item = usize> {
77    (0..count).flat_map(|degree| std::iter::repeat_n(degree, 2 * degree + 3))
78}
79
80impl SphereResidual {
81    /// The number of recorded layer boundaries.
82    #[must_use]
83    pub fn boundaries(&self) -> usize {
84        self.radii.len()
85    }
86
87    /// The shape of the T-matrix: `2 lmax (lmax + 2)` modes, as [`sw::modes`] lists them.
88    #[must_use]
89    pub fn shape(&self) -> (usize, usize) {
90        let lmax = self.degrees.len();
91        (2 * lmax * (lmax + 2), 2 * lmax * (lmax + 2))
92    }
93
94    /// The T-matrix tangent along changes of `k0`, the radii and the materials.
95    /// Material entries store changes of epsilon, mu and kappa. Each degree is
96    /// differentiated once and its block is repeated over the azimuthal orders.
97    pub fn pushforward(
98        &self,
99        k0: f64,
100        radii: &[f64],
101        materials: &[Material],
102    ) -> Result<DMatrix<Complex>> {
103        crate::coeffs::validate_layer_tangents(self.radii.len(), radii, materials)?;
104        if !k0.is_finite() {
105            return Err(Error::InvalidInput("k0 tangent must be finite".into()));
106        }
107        let sizes: Vec<_> = self
108            .radii
109            .iter()
110            .zip(radii)
111            .map(|(r, dr)| k0 * r + self.k0 * dr)
112            .collect();
113        let degrees = self
114            .degrees
115            .iter()
116            .map(|degree| degree.pushforward(&sizes, materials))
117            .collect::<Result<Vec<_>>>()?;
118        let (rows, cols) = self.shape();
119        let mut tangent = numerics::zeros(rows, cols)?;
120        for (block, degree) in block_degrees(degrees.len()).enumerate() {
121            tangent
122                .fixed_view_mut::<2, 2>(2 * block, 2 * block)
123                .copy_from(&to_mode_order(&degrees[degree]));
124        }
125        Ok(tangent)
126    }
127
128    /// Gradients of `k0`, the radii and the materials from `cotangent`, the gradient of
129    /// a real loss with respect to the T-matrix.
130    ///
131    /// The pullback runs on the calling thread and adds the degrees in order, so the
132    /// Rayon pool does not change it.
133    pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<SphereGradient> {
134        if cotangent.shape() != self.shape() || cotangent.iter().any(|z| !finite(*z)) {
135            return Err(Error::InvalidInput(
136                "invalid spherical T-matrix cotangent".into(),
137            ));
138        }
139        let mut blocks = vec![Matrix2::zeros(); self.degrees.len()];
140        for (block, degree) in block_degrees(self.degrees.len()).enumerate() {
141            let g = cotangent
142                .fixed_view::<2, 2>(2 * block, 2 * block)
143                .into_owned();
144            blocks[degree] += to_mode_order(&g);
145        }
146        let mut sum = MieGradient::zeros(self.radii.len());
147        for (residual, g) in self.degrees.iter().zip(blocks) {
148            sum.accumulate(&residual.pullback(&g)?);
149        }
150        // The size parameters k0 r carry both the radius and the k0 gradients. The
151        // matrix-free cluster applies this chain rule per degree instead
152        // (cluster::IterativeSphereCluster::local_gradients), which rounds differently.
153        let k0 = sum.sizes.iter().zip(&self.radii).map(|(g, r)| g * r).sum();
154        let radii = sum.sizes.iter().map(|g| self.k0 * g).collect();
155        Ok(SphereGradient {
156            k0,
157            layers: LayerGradient {
158                radii,
159                epsilon: sum.epsilon,
160                mu: sum.mu,
161                kappa: sum.kappa,
162            },
163        })
164    }
165}