treams_core/smatrix/
layers.rs

1//! S-matrices of planar multilayer stacks: one S-matrix of 2-by-2 blocks per
2//! transverse wavevector, built from [`interface()`](super::interface()) and
3//! [`add`](super::add), with analytic pullbacks.
4//!
5//! Upstream: `treams.SMatrices.stack` of interfaces and propagations.
6#![allow(clippy::indexing_slicing)] // Validated layer counts and two-polarization channels.
7
8mod saved;
9
10use nalgebra::DMatrix;
11
12use super::{AddGradient, AddResidual, Blocks, InterfaceGradient, InterfaceResidual};
13use crate::{
14    Complex, Error, Result,
15    linalg::product,
16    numerics::{
17        finite,
18        parallel::{try_fold_ordered, try_map},
19    },
20    pw::wave_vector_z,
21};
22
23/// One interior layer: propagation across it, then the interface above it.
24#[derive(Debug)]
25struct Step {
26    /// Normal wavenumber of each polarization in the layer.
27    normal: [Complex; 2],
28    /// Phase `exp(i kz d)` of each polarization across the layer.
29    phase: [Complex; 2],
30    /// Blocks 0, 1 and 3 of the stack below the layer.
31    below: [DMatrix<Complex>; 3],
32    boundary: AddResidual,
33    interface: InterfaceResidual,
34}
35
36/// The stack of one transverse wavevector: the lowest interface, then one step per
37/// interior layer.
38#[derive(Debug)]
39struct Channel {
40    initial: InterfaceResidual,
41    steps: Vec<Step>,
42}
43
44/// What [`layer_stack`] saves for its pullback: the solves of every transverse
45/// wavevector, each on its own, without a dense matrix over all of them.
46///
47/// The pullback adds the medium and thickness gradients of all wavevectors in chunks of
48/// consecutive wavevectors fixed by their count, and the chunk sums in chunk order
49/// (`numerics::parallel::try_fold_ordered`), so the thread count does not change them.
50/// Each `q` gradient belongs to one wavevector, which writes it in place.
51#[derive(Debug)]
52pub struct LayerStackResidual {
53    ks: Vec<[Complex; 2]>,
54    q: Vec<[f64; 2]>,
55    thickness: Vec<f64>,
56    channels: Vec<Channel>,
57    fixed_q: bool,
58}
59
60/// Multilayer parameter cotangents under the real Hermitian pairing.
61#[derive(Debug)]
62pub struct LayerStackGradient {
63    /// Two wavenumber cotangents per medium, ordered from the negative side (below) to
64    /// the positive side (above).
65    pub ks: Vec<[Complex; 2]>,
66    /// Impedance cotangents per medium.
67    pub zs: Vec<Complex>,
68    /// Real transverse-component cotangents per channel; zero when the forward holds
69    /// `q` fixed.
70    pub q: Vec<[f64; 2]>,
71    /// Real thickness cotangents for interior media.
72    pub thickness: Vec<f64>,
73}
74
75/// Place the propagation `P = diag(phase)` across a layer above the stack `below`.
76///
77/// Propagation reflects nothing, so the Redheffer product reduces to the closed
78/// form `[P V0, P V1 P, V2, V3 P]`. Returns it with the blocks its pullback reads.
79fn propagate(below: Blocks, phase: [Complex; 2]) -> (Blocks, [DMatrix<Complex>; 3]) {
80    let [v0, v1, v2, v3] = below;
81    // The dense products are those of `smatrix::add` with the exact identity
82    // operator of a reflectionless upper stack, so the blocks are bitwise equal to
83    // the generic composition; eigen-orderings of downstream bands depend on it.
84    let diagonal = DMatrix::from_fn(
85        2,
86        2,
87        |i, j| {
88            if i == j { phase[i] } else { Complex::default() }
89        },
90    );
91    let spaced = [
92        product(&diagonal, &v0),
93        product(&diagonal, &product(&v1, &diagonal)),
94        v2,
95        product(&v3, &diagonal),
96    ];
97    (spaced, [v0, v1, v3])
98}
99
100/// Pull the cotangent `g` of [`propagate`] back to the stack below and the phases.
101fn propagate_pullback(
102    g: Blocks,
103    below: &[DMatrix<Complex>; 3],
104    phase: [Complex; 2],
105) -> (Blocks, [Complex; 2]) {
106    let [v0, v1, v3] = below;
107    let mut gradient = [Complex::default(); 2];
108    for i in 0..2 {
109        for j in 0..2 {
110            gradient[i] +=
111                g[0][(i, j)] * v0[(i, j)].conj() + g[1][(i, j)] * (v1[(i, j)] * phase[j]).conj();
112            gradient[j] +=
113                g[1][(i, j)] * (phase[i] * v1[(i, j)]).conj() + g[3][(i, j)] * v3[(i, j)].conj();
114        }
115    }
116    let [g0, g1, g2, g3] = g;
117    let conjugate = phase.map(|p| p.conj());
118    let previous = [
119        DMatrix::from_fn(2, 2, |i, j| conjugate[i] * g0[(i, j)]),
120        DMatrix::from_fn(2, 2, |i, j| conjugate[i] * g1[(i, j)] * conjugate[j]),
121        g2,
122        DMatrix::from_fn(2, 2, |i, j| g3[(i, j)] * conjugate[j]),
123    ];
124    (previous, gradient)
125}
126
127/// Stack layers independently for each transverse wavevector; the output is one S-matrix
128/// of four 2-by-2 blocks per wavevector.
129///
130/// `ks` holds the wavenumbers of polarizations 0 and 1 and `zs` the impedance of every
131/// medium, from the negative side (below) to the positive side (above); `thickness`
132/// holds the interior layers in the same order. `axis` is the Cartesian normal of the
133/// layers, and each `q` holds the two transverse components after it in cyclic order.
134/// With `fixed_q`, the pullback holds the transverse components fixed and returns
135/// zero `q` gradients.
136///
137/// Upstream: `treams.SMatrices.stack` of alternating `SMatrices.interface` and
138/// `SMatrices.propagation`, here for each transverse wavevector separately.
139pub fn layer_stack(
140    ks: Vec<[Complex; 2]>,
141    zs: &[Complex],
142    q: Vec<[f64; 2]>,
143    thickness: &[f64],
144    axis: usize,
145    fixed_q: bool,
146) -> Result<(Vec<Blocks>, LayerStackResidual)> {
147    if ks.len() < 2
148        || ks.len() != zs.len()
149        || ks.len() != thickness.len() + 2
150        || q.is_empty()
151        || thickness.iter().any(|&d| !d.is_finite() || d < 0.0)
152    {
153        return Err(Error::InvalidInput("layers require at least two media, matching impedances, nonempty transverse channels and nonnegative interior thicknesses".into()));
154    }
155    let results = try_map(q.len(), q.len() > 1, |channel| -> Result<_> {
156        let q = q[channel];
157        let mut steps = Vec::with_capacity(thickness.len());
158        let (mut value, initial) =
159            super::interface([ks[0], ks[1]], [zs[0], zs[1]], q, axis, fixed_q)?;
160        for (layer, &d) in thickness.iter().enumerate() {
161            let medium = layer + 1;
162            // Outgoing normal wavenumbers keep |phase| <= 1 for d >= 0.
163            let normal = ks[medium].map(|k| wave_vector_z(q[0].into(), q[1].into(), k));
164            let phase = normal.map(|kz| (Complex::i() * (kz * d)).exp());
165            let (spaced, below) = propagate(value, phase);
166            let (matching, interface) = super::interface(
167                [ks[medium], ks[medium + 1]],
168                [zs[medium], zs[medium + 1]],
169                q,
170                axis,
171                fixed_q,
172            )?;
173            let boundary;
174            (value, boundary) = super::add(spaced, matching)?;
175            steps.push(Step {
176                normal,
177                phase,
178                below,
179                boundary,
180                interface,
181            });
182        }
183        Ok((value, Channel { initial, steps }))
184    })?;
185    let (values, channels) = results.into_iter().unzip();
186    Ok((
187        values,
188        LayerStackResidual {
189            ks,
190            q,
191            thickness: thickness.to_vec(),
192            channels,
193            fixed_q,
194        },
195    ))
196}
197
198impl LayerStackGradient {
199    /// Add the cotangents of another partial sum, entry by entry.
200    fn add(&mut self, other: &Self) {
201        for (a, b) in self
202            .ks
203            .iter_mut()
204            .flatten()
205            .chain(&mut self.zs)
206            .zip(other.ks.iter().flatten().chain(&other.zs))
207        {
208            *a += b;
209        }
210        for (a, b) in self
211            .q
212            .iter_mut()
213            .flatten()
214            .chain(&mut self.thickness)
215            .zip(other.q.iter().flatten().chain(&other.thickness))
216        {
217            *a += b;
218        }
219    }
220
221    /// Add the gradient of the interface between `medium` and the medium above it; its
222    /// transverse-wavevector part goes to `q`, the gradient of the channel's `q`.
223    fn add_interface(&mut self, medium: usize, q: &mut [f64; 2], gradient: InterfaceGradient) {
224        let InterfaceGradient {
225            ks: gk,
226            z: gz,
227            q: gq,
228        } = gradient;
229        for side in 0..2 {
230            for (target, g) in self.ks[medium + side].iter_mut().zip(gk[side]) {
231                *target += g;
232            }
233            self.zs[medium + side] += gz[side];
234        }
235        for (target, g) in q.iter_mut().zip(gq) {
236            *target += g;
237        }
238    }
239}
240
241impl LayerStackResidual {
242    /// Number of media, including the exterior half-spaces.
243    #[must_use]
244    pub fn medium_count(&self) -> usize {
245        self.ks.len()
246    }
247
248    /// Number of independent transverse channels.
249    #[must_use]
250    pub fn channel_count(&self) -> usize {
251        self.q.len()
252    }
253
254    /// Propagate one parameter direction through the independent two-polarization
255    /// channels, reusing every saved interface and internal-field factorization.
256    pub fn pushforward(
257        &self,
258        ks: &[[Complex; 2]],
259        zs: &[Complex],
260        q: &[[f64; 2]],
261        thickness: &[f64],
262    ) -> Result<Vec<Blocks>> {
263        if ks.len() != self.ks.len()
264            || zs.len() != self.ks.len()
265            || q.len() != self.q.len()
266            || thickness.len() != self.thickness.len()
267            || ks.iter().flatten().chain(zs).any(|&v| !finite(v))
268            || q.iter().flatten().chain(thickness).any(|v| !v.is_finite())
269        {
270            return Err(Error::InvalidInput("invalid layer-stack tangents".into()));
271        }
272        try_map(self.channels.len(), self.channels.len() > 1, |i| {
273            let channel = &self.channels[i];
274            let dq = if self.fixed_q { [0.0; 2] } else { q[i] };
275            let mut tangent = channel
276                .initial
277                .pushforward([ks[0], ks[1]], [zs[0], zs[1]], dq)?;
278            for (layer, step) in channel.steps.iter().enumerate() {
279                let medium = layer + 1;
280                let phase_tangent: [Complex; 2] = std::array::from_fn(|pol| {
281                    let normal_tangent = (self.ks[medium][pol] * ks[medium][pol]
282                        - self.q[i][0] * dq[0]
283                        - self.q[i][1] * dq[1])
284                        / step.normal[pol];
285                    Complex::i()
286                        * step.phase[pol]
287                        * (self.thickness[layer] * normal_tangent
288                            + step.normal[pol] * thickness[layer])
289                });
290                let [dv0, dv1, dv2, dv3] = tangent;
291                let [v0, v1, v3] = &step.below;
292                let phase = step.phase;
293                let spaced = [
294                    DMatrix::from_fn(2, 2, |i, j| {
295                        phase_tangent[i] * v0[(i, j)] + phase[i] * dv0[(i, j)]
296                    }),
297                    DMatrix::from_fn(2, 2, |i, j| {
298                        phase_tangent[i] * v1[(i, j)] * phase[j]
299                            + phase[i] * dv1[(i, j)] * phase[j]
300                            + phase[i] * v1[(i, j)] * phase_tangent[j]
301                    }),
302                    dv2,
303                    DMatrix::from_fn(2, 2, |i, j| {
304                        dv3[(i, j)] * phase[j] + v3[(i, j)] * phase_tangent[j]
305                    }),
306                ];
307                let boundary = step.interface.pushforward(
308                    [ks[medium], ks[medium + 1]],
309                    [zs[medium], zs[medium + 1]],
310                    dq,
311                )?;
312                tangent = step.boundary.pushforward(&spaced, &boundary)?;
313            }
314            Ok(tangent)
315        })
316    }
317
318    /// Reuse every two-polarization solve; accumulate shared medium and thickness derivatives.
319    pub fn pullback(&self, cotangent: Vec<Blocks>) -> Result<LayerStackGradient> {
320        if cotangent.len() != self.q.len()
321            || cotangent
322                .iter()
323                .flatten()
324                .any(|b| b.shape() != (2, 2) || b.iter().any(|&v| !finite(v)))
325        {
326            return Err(Error::InvalidInput(
327                "invalid compact layer-stack cotangent".into(),
328            ));
329        }
330        let Self {
331            ks,
332            q,
333            thickness,
334            channels,
335            fixed_q,
336        } = self;
337        // Each channel writes its own `q` gradient in place; the partial sums carry only
338        // the medium and thickness gradients that all channels share.
339        let mut q_gradients = vec![[0.0; 2]; q.len()];
340        let items: Vec<_> = channels
341            .iter()
342            .zip(cotangent)
343            .zip(&mut q_gradients)
344            .collect();
345        let mut result = try_fold_ordered(
346            items,
347            true,
348            || LayerStackGradient {
349                ks: vec![[Complex::default(); 2]; ks.len()],
350                zs: vec![Complex::default(); ks.len()],
351                q: Vec::new(),
352                thickness: vec![0.0; ks.len() - 2],
353            },
354            |mut result, i, ((channel, mut cotangent), q_gradient)| -> Result<_> {
355                let wavevector = q[i];
356                for (layer, step) in channel.steps.iter().enumerate().rev() {
357                    let medium = layer + 1;
358                    let AddGradient {
359                        lower: spaced_g,
360                        upper: interface_g,
361                    } = step.boundary.pullback(&cotangent)?;
362                    let interface_g = step.interface.pullback(&interface_g)?;
363                    result.add_interface(medium, q_gradient, interface_g);
364                    let (previous, phase_g) = propagate_pullback(spaced_g, &step.below, step.phase);
365                    let d = thickness[layer];
366                    for (pol, (&phase, &normal)) in step.phase.iter().zip(&step.normal).enumerate()
367                    {
368                        let phase_g = phase_g[pol];
369                        // phase = exp(i kz d): dphase = i phase (d dkz + kz dd).
370                        let slope = Complex::i() * phase;
371                        result.thickness[layer] += (phase_g.conj() * slope * normal).re;
372                        let normal_g = phase_g * (slope * d).conj();
373                        // kz = sqrt(k^2 - |q|^2): dkz = (k dk - q . dq) / kz.
374                        result.ks[medium][pol] += normal_g * (ks[medium][pol] / normal).conj();
375                        if !fixed_q {
376                            for (target, &component) in q_gradient.iter_mut().zip(&wavevector) {
377                                *target -= (normal_g.conj() * component / normal).re;
378                            }
379                        }
380                    }
381                    cotangent = previous;
382                }
383                let initial_g = channel.initial.pullback(&cotangent)?;
384                result.add_interface(0, q_gradient, initial_g);
385                Ok(result)
386            },
387            |mut total, partial| {
388                total.add(&partial);
389                total
390            },
391        )?;
392        result.q = q_gradients;
393        Ok(result)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::{Blocks, layer_stack};
400    use crate::{
401        Complex,
402        test_support::{assert_same_bits_on_pools, bits, patterned},
403    };
404
405    /// The medium and thickness gradients add in chunks fixed by the channel count:
406    /// with more channels than chunks, every gradient repeats bit for bit on every pool
407    /// size.
408    #[test]
409    fn pullback_does_not_depend_on_the_thread_count() {
410        let c = Complex::new;
411        let ks = vec![
412            [c(1.0, 0.0); 2],
413            [c(1.7, 0.02), c(1.8, 0.01)],
414            [c(1.3, 0.0), c(1.35, 0.0)],
415            [c(2.1, 0.05); 2],
416            [c(1.0, 0.0); 2],
417        ];
418        let zs = [
419            c(1.0, 0.0),
420            c(0.7, 0.01),
421            c(0.8, 0.0),
422            c(0.6, 0.02),
423            c(1.0, 0.0),
424        ];
425        let thickness = [0.3, 0.45, 0.2];
426        let channels = 100_u32;
427        let q: Vec<[f64; 2]> = (0..channels)
428            .map(|j| {
429                let t = f64::from(j);
430                [0.9 * (0.7 * t).sin(), 0.8 * (1.3 * t).cos()]
431            })
432            .collect();
433        let g: Vec<Blocks> = (0..channels)
434            .map(|j| [0.0, 0.25, 0.5, 0.75].map(|seed| patterned(2, 2, f64::from(j) + seed)))
435            .collect();
436        assert_same_bits_on_pools(|| {
437            let (_, residual) =
438                layer_stack(ks.clone(), &zs, q.clone(), &thickness, 2, false).unwrap();
439            let g = residual.pullback(g.clone()).unwrap();
440            bits(&[&g.ks, &g.zs, &g.q, &g.thickness])
441        });
442    }
443}