treams_core/tmatrix/
cylinder.rs

1//! T-matrices of multilayer chiral cylinders from their Mie coefficients.
2//!
3//! Upstream: `treams.TMatrixC.cylinder`.
4#![allow(clippy::indexing_slicing)] // Block, solve and axial-wavenumber indices built here.
5
6use crate::{
7    Complex, Error, MAX_DEGREE, Result,
8    coeffs::{LayerGradient, Material, MieCylResidual, mie_cyl},
9    numerics::{self, finite, label_bits},
10};
11
12mod saved;
13
14/// What [`cylinder`] saves for its pullback: one boundary solve per `(kz, m)` pair,
15/// shared with the mirror pair `(-kz, -m)` when both are requested.
16#[derive(Debug)]
17pub struct CylinderResidual {
18    blocks: Vec<Block>,
19    solves: Vec<MieCylResidual>,
20    kz_count: usize,
21    boundaries: usize,
22}
23
24/// One `(kz, m)` block: its axial wavenumber index and the boundary solve it
25/// shares with its mirror image `(-kz, -m)` under a half turn about a transverse axis,
26/// which leaves the cylinder and hence the coefficients unchanged.
27#[derive(Clone, Copy, Debug)]
28struct Block {
29    kz_index: usize,
30    solve: usize,
31    mirrored: bool,
32}
33
34/// Gradients of the inputs of [`cylinder`], in the order of its arguments.
35#[derive(Debug)]
36pub struct CylinderGradient {
37    /// Gradient of each axial wavenumber in `kzs`.
38    pub kzs: Vec<f64>,
39    /// Gradient of the vacuum wavenumber.
40    pub k0: f64,
41    /// Gradients of the radii and the materials.
42    pub layers: LayerGradient,
43}
44
45/// The T-matrix of concentric multilayer chiral cylinders, ordered by `kzs`, then
46/// ascending order `m` from `-mmax` to `mmax`, then positive and negative helicity.
47///
48/// The boundary solves of [`mie_cyl`] run in parallel. A block `(kz, m)` whose mirror
49/// `(-kz, -m)` is requested as well reuses the mirror's solve when `kz < 0`, or
50/// `kz = 0` and `m < 0`.
51///
52/// Upstream: `treams.TMatrixC.cylinder`. Differences: the shared mirror solves, which
53/// agree with separate solves to rounding.
54pub fn cylinder(
55    kzs: &[f64],
56    mmax: u32,
57    k0: f64,
58    radii: &[f64],
59    materials: &[Material],
60) -> Result<(nalgebra::DMatrix<Complex>, CylinderResidual)> {
61    use rayon::prelude::*;
62    if kzs.is_empty() || kzs.iter().any(|k| !k.is_finite()) || mmax > MAX_DEGREE.unsigned_abs() {
63        return Err(Error::InvalidInput(
64            // 128 is MAX_DEGREE.
65            "require finite nonempty kzs and mmax <= 128".into(),
66        ));
67    }
68    let mut seen = std::collections::HashSet::new();
69    if kzs.iter().any(|&kz| !seen.insert(label_bits(kz))) {
70        return Err(Error::InvalidInput(
71            "axial wavenumbers must be unique".into(),
72        ));
73    }
74    let bound = i32::try_from(mmax).map_err(|_| Error::InvalidInput("invalid mmax".into()))?;
75    let mirrored =
76        |kz: f64, m: i32| (kz < 0.0 || (kz == 0.0 && m < 0)) && seen.contains(&label_bits(-kz));
77    let labels: Vec<_> = kzs
78        .iter()
79        .enumerate()
80        .flat_map(|(index, &kz)| (-bound..=bound).map(move |m| (index, kz, m)))
81        .collect();
82    let mut solved = Vec::new();
83    let mut solve = std::collections::HashMap::new();
84    for &(_, kz, m) in labels.iter().filter(|&&(_, kz, m)| !mirrored(kz, m)) {
85        solve.insert((label_bits(kz), m), solved.len());
86        solved.push((kz, m));
87    }
88    let solves = crate::threads::install(|| {
89        solved
90            .par_iter()
91            .map(|&(kz, m)| mie_cyl(kz, m, k0, radii, materials))
92            .collect::<Result<Vec<_>>>()
93    })?;
94    let blocks = labels
95        .iter()
96        .map(|&(index, kz, m)| {
97            let mirrored = mirrored(kz, m);
98            let (kz, m) = if mirrored { (-kz, -m) } else { (kz, m) };
99            Ok(Block {
100                kz_index: index,
101                solve: *solve
102                    .get(&(label_bits(kz), m))
103                    .ok_or_else(|| Error::InvalidInput("missing cylinder block".into()))?,
104                mirrored,
105            })
106        })
107        .collect::<Result<Vec<_>>>()?;
108    let mut value = numerics::zeros(2 * blocks.len(), 2 * blocks.len())?;
109    for (index, block) in blocks.iter().enumerate() {
110        value
111            .fixed_view_mut::<2, 2>(2 * index, 2 * index)
112            .copy_from(&crate::coeffs::to_mode_order(solves[block.solve].value()));
113    }
114    Ok((
115        value,
116        CylinderResidual {
117            blocks,
118            solves,
119            kz_count: kzs.len(),
120            boundaries: radii.len(),
121        },
122    ))
123}
124
125impl CylinderResidual {
126    /// The counts of recorded axial wavenumbers and layer boundaries.
127    #[must_use]
128    pub const fn input_counts(&self) -> (usize, usize) {
129        (self.kz_count, self.boundaries)
130    }
131
132    /// The shape of the T-matrix: two helicities for each `(kz, m)` block.
133    #[must_use]
134    pub fn shape(&self) -> (usize, usize) {
135        (2 * self.blocks.len(), 2 * self.blocks.len())
136    }
137
138    /// The T-matrix tangent along changes of the axial wavenumbers, `k0`, radii and
139    /// materials. Material entries store changes of epsilon, mu and kappa.
140    /// Mirror blocks reuse their cached solve, with the sign of their own `kz`
141    /// direction reversed; their axial directions need not preserve the symmetry.
142    pub fn pushforward(
143        &self,
144        kzs: &[f64],
145        k0: f64,
146        radii: &[f64],
147        materials: &[Material],
148    ) -> Result<nalgebra::DMatrix<Complex>> {
149        use rayon::prelude::*;
150        crate::coeffs::validate_layer_tangents(self.boundaries, radii, materials)?;
151        if kzs.len() != self.kz_count || kzs.iter().any(|k| !k.is_finite()) || !k0.is_finite() {
152            return Err(Error::InvalidInput(
153                "wavenumber tangents must be finite and match the recorded inputs".into(),
154            ));
155        }
156        let blocks = crate::threads::install(|| {
157            self.blocks
158                .par_iter()
159                .map(|block| {
160                    let kz = kzs[block.kz_index] * if block.mirrored { -1.0 } else { 1.0 };
161                    self.solves[block.solve].pushforward(kz, k0, radii, materials)
162                })
163                .collect::<Result<Vec<_>>>()
164        })?;
165        let (rows, cols) = self.shape();
166        let mut tangent = numerics::zeros(rows, cols)?;
167        for (index, block) in blocks.iter().enumerate() {
168            tangent
169                .fixed_view_mut::<2, 2>(2 * index, 2 * index)
170                .copy_from(&crate::coeffs::to_mode_order(block));
171        }
172        Ok(tangent)
173    }
174
175    /// Gradients of `kzs`, `k0`, the radii and the materials from `cotangent`, the
176    /// gradient of a real loss with respect to the T-matrix.
177    ///
178    /// The blocks run in parallel and their gradients add in block order, so the Rayon
179    /// pool does not change them.
180    pub fn pullback(&self, cotangent: &nalgebra::DMatrix<Complex>) -> Result<CylinderGradient> {
181        use rayon::prelude::*;
182        if cotangent.shape() != self.shape() || cotangent.iter().any(|&g| !finite(g)) {
183            return Err(Error::InvalidInput(
184                "invalid cylinder T-matrix cotangent".into(),
185            ));
186        }
187        let solves = &self.solves;
188        let gradients = crate::threads::install(|| {
189            self.blocks
190                .par_iter()
191                .enumerate()
192                .map(|(index, block)| {
193                    let g = cotangent
194                        .fixed_view::<2, 2>(2 * index, 2 * index)
195                        .into_owned();
196                    let gradient =
197                        solves[block.solve].pullback(&crate::coeffs::to_mode_order(&g))?;
198                    // The mirror block depends on its own kz with the opposite sign.
199                    let kz = if block.mirrored {
200                        -gradient.kz
201                    } else {
202                        gradient.kz
203                    };
204                    Ok((kz, gradient))
205                })
206                .collect::<Result<Vec<_>>>()
207        })?;
208        let mut result = CylinderGradient {
209            kzs: vec![0.0; self.kz_count],
210            k0: 0.0,
211            layers: LayerGradient::zeros(self.boundaries),
212        };
213        for (block, (kz, gradient)) in self.blocks.iter().zip(gradients) {
214            result.kzs[block.kz_index] += kz;
215            result.k0 += gradient.k0;
216            result.layers.accumulate(&gradient.layers);
217        }
218        Ok(result)
219    }
220}