treams_core/smatrix/
interface.rs

1//! Chiral Fresnel and Cartesian planar interfaces, and propagation of up/down
2//! reference planes.
3//!
4//! Upstream: `treams.coeffs.fresnel`, `treams.SMatrices.interface` and
5//! `treams.SMatrices.propagation`.
6
7mod saved;
8
9use nalgebra::DMatrix;
10
11use super::{Blocks, checked_dimension, dimension, identity_blocks};
12use crate::{
13    Complex, Error, Result,
14    numerics::{Jet, finite},
15};
16
17/// The chiral Fresnel coefficients `res[out][in][out pol][in pol]`, transcribed from
18/// upstream `treams.coeffs.fresnel` with its variable names.
19///
20/// `out` and `in` index the propagation direction (0 = up, 1 = down), so `res[0][0]`
21/// transmits upward from the negative medium. The inputs index the medium first,
22/// negative side (below) then positive side (above), and the polarization second.
23fn fresnel_values<const N: usize>(
24    ks: [[Jet<N>; 2]; 2],
25    kzs: [[Jet<N>; 2]; 2],
26    zs: [Jet<N>; 2],
27) -> [[[[Jet<N>; 2]; 2]; 2]; 2] {
28    let ap = std::array::from_fn::<_, 2, _>(|i| ks[i][0] * kzs[i][1] + ks[i][1] * kzs[i][0]);
29    let am = std::array::from_fn::<_, 2, _>(|i| ks[i][0] * kzs[i][1] - ks[i][1] * kzs[i][0]);
30    let bp = ks[0][1] * kzs[1][1] + ks[1][1] * kzs[0][1];
31    let cp = ks[0][0] * kzs[1][0] + ks[1][0] * kzs[0][0];
32    let zs_diff = zs[0] - zs[1];
33    let zs_prod = 4.0 * zs[0] * zs[1];
34    let pref = 1.0 / (zs_diff * zs_diff * ap[0] * ap[1] + zs_prod * bp * cp);
35    let mut res = [[[[Jet::default(); 2]; 2]; 2]; 2];
36    for i in 0..2 {
37        let j = 1 - i;
38        res[i][i][0][0] = (zs[0] + zs[1]) * ks[j][0] * kzs[i][0] * bp * zs[j] * 4.0 * pref;
39        res[i][i][0][1] = -(zs[i] - zs[j])
40            * ks[j][0]
41            * kzs[i][1]
42            * (ks[j][1] * kzs[i][0] - ks[i][0] * kzs[j][1])
43            * zs[j]
44            * 4.0
45            * pref;
46        res[i][i][1][0] = -(zs[i] - zs[j])
47            * ks[j][1]
48            * kzs[i][0]
49            * (ks[j][0] * kzs[i][1] - ks[i][1] * kzs[j][0])
50            * zs[j]
51            * 4.0
52            * pref;
53        res[i][i][1][1] = (zs[0] + zs[1]) * ks[j][1] * kzs[i][1] * cp * zs[j] * 4.0 * pref;
54        res[j][i][0][0] = (-zs_diff * zs_diff * am[i] * ap[j]
55            - zs_prod * bp * (ks[i][0] * kzs[j][0] - ks[j][0] * kzs[i][0]))
56            * pref;
57        res[j][i][0][1] =
58            -2.0 * (zs[j] * zs[j] - zs[i] * zs[i]) * ks[i][0] * kzs[i][1] * ap[j] * pref;
59        res[j][i][1][0] =
60            -2.0 * (zs[j] * zs[j] - zs[i] * zs[i]) * ks[i][1] * kzs[i][0] * ap[j] * pref;
61        res[j][i][1][1] = (zs_diff * zs_diff * am[i] * ap[j]
62            - zs_prod * cp * (ks[i][1] * kzs[j][1] - ks[j][1] * kzs[i][1]))
63            * pref;
64    }
65    res
66}
67
68/// What [`fresnel`] saves for its pullback: the wavenumbers, the normal wavenumbers and
69/// the impedances. The pullback differentiates the closed forms again instead of keeping
70/// a Jacobian.
71#[derive(Debug)]
72pub struct FresnelResidual {
73    ks: [[Complex; 2]; 2],
74    kzs: [[Complex; 2]; 2],
75    zs: [Complex; 2],
76}
77
78/// Planar interface from full and normal wavenumbers and impedances, each indexed by
79/// medium (negative side, then positive side) and then by polarization.
80///
81/// Returns four helicity scattering blocks with polarization order (0, 1).
82///
83/// Upstream: `treams.coeffs.fresnel(ks, kzs, zs)`.
84pub fn fresnel(
85    ks: [[Complex; 2]; 2],
86    kzs: [[Complex; 2]; 2],
87    zs: [Complex; 2],
88) -> Result<(Blocks, FresnelResidual)> {
89    if ks
90        .iter()
91        .flatten()
92        .chain(kzs.iter().flatten())
93        .chain(zs.iter())
94        .any(|&v| !finite(v))
95    {
96        return Err(Error::InvalidInput("Fresnel inputs must be finite".into()));
97    }
98    if ks[0] == ks[1]
99        && kzs[0] == kzs[1]
100        && zs[0] == zs[1]
101        && zs[0] != Complex::default()
102        && ks[0].iter().all(|&k| k != Complex::default())
103        && kzs[0].contains(&Complex::default())
104    {
105        // Identical media have no boundary. This is the exact continuation of
106        // transmission through a grazing channel, whose Fresnel formula is 0/0.
107        return Ok((identity_blocks(2), FresnelResidual { ks, kzs, zs }));
108    }
109    let values = fresnel_values::<0>(
110        ks.map(|r| r.map(Jet::constant)),
111        kzs.map(|r| r.map(Jet::constant)),
112        zs.map(Jet::constant),
113    );
114    let value =
115        std::array::from_fn(|b| DMatrix::from_fn(2, 2, |i, j| values[b / 2][b % 2][i][j].value));
116    dimension(&value).map_err(|_| Error::Singular)?;
117    Ok((value, FresnelResidual { ks, kzs, zs }))
118}
119
120/// Cotangents of the inputs of [`fresnel`], in the order of its arguments; each array
121/// lists the medium on the negative side (below), then the positive side (above).
122#[derive(Clone, Copy, Debug)]
123pub struct FresnelGradient {
124    /// Cotangents of the full wavenumbers `ks` of polarizations 0 and 1.
125    pub ks: [[Complex; 2]; 2],
126    /// Cotangents of the normal wavenumbers `kzs` of polarizations 0 and 1.
127    pub kz: [[Complex; 2]; 2],
128    /// Cotangents of the impedances `zs`.
129    pub z: [Complex; 2],
130}
131
132impl FresnelResidual {
133    /// Propagate one joint wavenumber, normal-wavenumber and impedance direction.
134    pub fn pushforward(
135        &self,
136        ks: [[Complex; 2]; 2],
137        kz: [[Complex; 2]; 2],
138        z: [Complex; 2],
139    ) -> Result<Blocks> {
140        if ks
141            .iter()
142            .flatten()
143            .chain(kz.iter().flatten())
144            .chain(&z)
145            .any(|&v| !finite(v))
146        {
147            return Err(Error::InvalidInput(
148                "Fresnel tangents must be finite".into(),
149            ));
150        }
151        let values = fresnel_values(
152            std::array::from_fn(|i| {
153                std::array::from_fn(|j| Jet {
154                    value: self.ks[i][j],
155                    derivative: [ks[i][j]],
156                })
157            }),
158            std::array::from_fn(|i| {
159                std::array::from_fn(|j| Jet {
160                    value: self.kzs[i][j],
161                    derivative: [kz[i][j]],
162                })
163            }),
164            std::array::from_fn(|i| Jet {
165                value: self.zs[i],
166                derivative: [z[i]],
167            }),
168        );
169        if values
170            .iter()
171            .flatten()
172            .flatten()
173            .flatten()
174            .any(|v| !v.finite())
175        {
176            return Err(Error::InvalidInput(
177                "Fresnel derivative is undefined for this grazing-channel limit".into(),
178            ));
179        }
180        Ok(std::array::from_fn(|b| {
181            DMatrix::from_fn(2, 2, |i, j| values[b / 2][b % 2][i][j].derivative[0])
182        }))
183    }
184
185    /// The gradients of all ten complex inputs under the real pairing `Re Σ conj(g)·dx`.
186    pub fn pullback(&self, cotangent: &Blocks) -> Result<FresnelGradient> {
187        // Jet seed offsets: `ks[i][j]` at `KS + 2 i + j`, `kzs[i][j]` at `KZS + 2 i + j`
188        // and `zs[i]` at `ZS + i`.
189        const KS: usize = 0;
190        const KZS: usize = 4;
191        const ZS: usize = 8;
192        checked_dimension(cotangent, 2, "Fresnel cotangent blocks must be 2 by 2")?;
193        let ks = std::array::from_fn(|i| {
194            std::array::from_fn(|j| Jet::<10>::variable(self.ks[i][j], KS + 2 * i + j))
195        });
196        let kzs = std::array::from_fn(|i| {
197            std::array::from_fn(|j| Jet::variable(self.kzs[i][j], KZS + 2 * i + j))
198        });
199        let zs = std::array::from_fn(|i| Jet::variable(self.zs[i], ZS + i));
200        let values = fresnel_values(ks, kzs, zs);
201        if values
202            .iter()
203            .flatten()
204            .flatten()
205            .flatten()
206            .any(|v| !v.finite())
207        {
208            return Err(Error::InvalidInput(
209                "Fresnel derivative is undefined for this grazing-channel limit".into(),
210            ));
211        }
212        let mut result = [Complex::default(); 10];
213        for (b, block) in cotangent.iter().enumerate() {
214            for i in 0..2 {
215                for j in 0..2 {
216                    for (gradient, d) in
217                        result.iter_mut().zip(values[b / 2][b % 2][i][j].derivative)
218                    {
219                        *gradient += block[(i, j)] * d.conj();
220                    }
221                }
222            }
223        }
224        Ok(FresnelGradient {
225            ks: std::array::from_fn(|i| std::array::from_fn(|j| result[KS + 2 * i + j])),
226            kz: std::array::from_fn(|i| std::array::from_fn(|j| result[KZS + 2 * i + j])),
227            z: [result[ZS], result[ZS + 1]],
228        })
229    }
230}
231
232/// The four 2-by-2 scattering blocks of one interface as a 4-by-4 matrix.
233type InterfaceMatrix = nalgebra::SMatrix<Complex, 4, 4>;
234
235/// One 4-by-4 matrix (`lhs` or `rhs`) of the tangential-field matching, as jets.
236type BoundaryJets<const N: usize> = [[Jet<N>; 4]; 4];
237
238/// The normal wavenumber `sqrt(k^2 - q.q)` on the branch with a nonnegative imaginary
239/// part.
240fn normal_component<const N: usize>(k: Jet<N>, q: [Jet<N>; 2]) -> Result<Jet<N>> {
241    let mut normal = (k * k - q[0] * q[0] - q[1] * q[1]).sqrt();
242    // A grazing wave still has finite tangential fields. Only the derivative
243    // of its square-root dispersion is singular, not the forward interface.
244    if N > 0 && normal.value == Complex::default() {
245        return Err(Error::InvalidInput(
246            "interface derivative is undefined at an exact diffraction threshold".into(),
247        ));
248    }
249    // The principal root has a nonnegative real part; take the outgoing branch.
250    if normal.value.im < 0.0 {
251        normal = -normal;
252    }
253    Ok(normal)
254}
255
256/// Tangential fields `[E_a, E_b, H_a, H_b]` of the plane waves in one medium, indexed
257/// by propagation direction (0 = up) and polarization; `a` and `b` are the two axes
258/// after `axis` in cyclic order.
259pub(super) fn port_boundary<const N: usize>(
260    ks: [Jet<N>; 2],
261    z: Jet<N>,
262    q: [Jet<N>; 2],
263    axis: usize,
264) -> Result<[[[Jet<N>; 4]; 2]; 2]> {
265    let mut waves = [[[Jet::default(); 4]; 2]; 2];
266    let a = (axis + 1) % 3;
267    let b = (axis + 2) % 3;
268    for (pol, &wave_number) in ks.iter().enumerate() {
269        let normal = normal_component(wave_number, q)?;
270        for (direction, polarizations) in waves.iter_mut().enumerate() {
271            let mut vector = [Jet::default(); 3];
272            vector[a] = q[0];
273            vector[b] = q[1];
274            vector[axis] = if direction == 0 { normal } else { -normal };
275            let e = crate::pw::polarization_from_inputs(vector, u8::from(pol != 0))?;
276            let impedance = -Complex::i() * (if pol == 0 { -1.0 } else { 1.0 }) / z;
277            polarizations[pol] = [e[a], e[b], impedance * e[a], impedance * e[b]];
278        }
279    }
280    Ok(waves)
281}
282
283/// The tangential-field matching `lhs s = rhs` of an interface. The columns of `lhs`
284/// are the outgoing waves (up in the positive medium, down in the negative one), those
285/// of `rhs` the incoming waves (up in the negative medium, down in the positive one).
286fn interface_boundary<const N: usize>(
287    ks: [[Jet<N>; 2]; 2],
288    zs: [Jet<N>; 2],
289    q: [Jet<N>; 2],
290    axis: usize,
291) -> Result<(BoundaryJets<N>, BoundaryJets<N>)> {
292    let waves = [
293        port_boundary(ks[0], zs[0], q, axis)?,
294        port_boundary(ks[1], zs[1], q, axis)?,
295    ];
296    let lhs = std::array::from_fn(|row| {
297        std::array::from_fn(|col| {
298            if col < 2 {
299                waves[1][0][col][row]
300            } else {
301                -waves[0][1][col - 2][row]
302            }
303        })
304    });
305    let rhs = std::array::from_fn(|row| {
306        std::array::from_fn(|col| {
307            if col < 2 {
308                waves[0][0][col][row]
309            } else {
310                -waves[1][1][col - 2][row]
311            }
312        })
313    });
314    Ok((lhs, rhs))
315}
316
317/// What [`interface()`] saves for its pullback: the media, the transverse wavevector,
318/// the axis, the inverse of the tangential-field matching matrix and the blocks.
319#[derive(Debug)]
320pub struct InterfaceResidual {
321    ks: [[Complex; 2]; 2],
322    zs: [Complex; 2],
323    q: [f64; 2],
324    axis: usize,
325    inverse: Option<InterfaceMatrix>,
326    /// The scattering blocks as one matrix over (up, down) outputs and inputs.
327    result: InterfaceMatrix,
328    fixed_q: bool,
329}
330
331/// Blocks `[[b0, b1], [b2, b3]]` of 2-by-2 polarization blocks as one 4-by-4 matrix.
332fn interface_matrix(blocks: &Blocks) -> InterfaceMatrix {
333    InterfaceMatrix::from_fn(|i, j| blocks[2 * (i / 2) + j / 2][(i % 2, j % 2)])
334}
335
336/// The four 2-by-2 polarization blocks of a 4-by-4 interface matrix.
337fn interface_blocks(matrix: &InterfaceMatrix) -> Blocks {
338    std::array::from_fn(|block| {
339        DMatrix::from_fn(2, 2, |i, j| {
340            matrix[(2 * (block / 2) + i, 2 * (block % 2) + j)]
341        })
342    })
343}
344
345/// Interface from wavenumbers and impedances, indexed by medium (negative side, then
346/// positive side), and the real transverse wavevector `q`.
347///
348/// `axis` is the Cartesian normal, and `q` holds the two components after it in cyclic
349/// order. Returns four helicity scattering blocks with polarization order (0, 1). With
350/// `fixed_q`, the pullback holds `q` fixed and returns a zero `q` gradient.
351///
352/// Upstream: `treams.SMatrices.interface`.
353pub fn interface(
354    ks: [[Complex; 2]; 2],
355    zs: [Complex; 2],
356    q: [f64; 2],
357    axis: usize,
358    fixed_q: bool,
359) -> Result<(Blocks, InterfaceResidual)> {
360    if axis > 2
361        || q.iter().any(|x| !x.is_finite())
362        || ks
363            .iter()
364            .flatten()
365            .chain(&zs)
366            .any(|&v| !finite(v) || v == Complex::default())
367    {
368        return Err(Error::InvalidInput("interface requires finite transverse components, nonzero finite wavenumbers/impedances, and a Cartesian normal".into()));
369    }
370    let (lhs, rhs) = interface_boundary::<0>(
371        ks.map(|k| k.map(Jet::constant)),
372        zs.map(Jet::constant),
373        q.map(Jet::constant),
374        axis,
375    )?;
376    let matrix = InterfaceMatrix::from_fn(|i, j| lhs[i][j].value);
377    let identical = ks[0] == ks[1] && zs[0] == zs[1];
378    let matched_grazing = identical
379        && ks[0]
380            .iter()
381            .any(|&k| k * k - q[0] * q[0] - q[1] * q[1] == Complex::default());
382    let inverse = if matched_grazing {
383        None
384    } else {
385        Some(matrix.lu().try_inverse().ok_or(Error::Singular)?)
386    };
387    // The same medium on both sides is transparent for every transverse
388    // wavevector. Tangential matching alone is ill-conditioned near grazing
389    // incidence and cannot distinguish the ports at it, so use the exact value;
390    // the saved inverse still differentiates it away from the threshold.
391    let result = match inverse {
392        Some(inverse) if !identical => inverse * InterfaceMatrix::from_fn(|i, j| rhs[i][j].value),
393        _ => InterfaceMatrix::identity(),
394    };
395    let value = interface_blocks(&result);
396    dimension(&value).map_err(|_| Error::Singular)?;
397    Ok((
398        value,
399        InterfaceResidual {
400            ks,
401            zs,
402            q,
403            axis,
404            inverse,
405            result,
406            fixed_q,
407        },
408    ))
409}
410
411/// Cotangents of the inputs of [`interface`], in the order of its arguments; the media
412/// are listed negative side (below), then positive side (above).
413#[derive(Clone, Copy, Debug)]
414pub struct InterfaceGradient {
415    /// Cotangents of the wavenumbers of polarizations 0 and 1 in each medium.
416    pub ks: [[Complex; 2]; 2],
417    /// Cotangents of the impedances `zs` of the media.
418    pub z: [Complex; 2],
419    /// Cotangents of the real transverse wavevector components; zero when the forward
420    /// holds `q` fixed.
421    pub q: [f64; 2],
422}
423
424impl InterfaceResidual {
425    /// Differentiate the tangential-field solve using its saved inverse and one
426    /// direction of local field derivatives.
427    pub fn pushforward(
428        &self,
429        ks: [[Complex; 2]; 2],
430        z: [Complex; 2],
431        q: [f64; 2],
432    ) -> Result<Blocks> {
433        if ks.iter().flatten().chain(&z).any(|&v| !finite(v)) || q.iter().any(|v| !v.is_finite()) {
434            return Err(Error::InvalidInput(
435                "interface tangents must be finite".into(),
436            ));
437        }
438        let inverse = self.inverse.ok_or_else(|| {
439            Error::InvalidInput(
440                "interface derivative is undefined at an exact diffraction threshold".into(),
441            )
442        })?;
443        let fixed_q = self.fixed_q || (self.axis == 2 && self.q.iter().all(|&v| v == 0.0));
444        let (lhs, rhs) = interface_boundary(
445            std::array::from_fn(|i| {
446                std::array::from_fn(|j| Jet {
447                    value: self.ks[i][j],
448                    derivative: [ks[i][j]],
449                })
450            }),
451            std::array::from_fn(|i| Jet {
452                value: self.zs[i],
453                derivative: [z[i]],
454            }),
455            std::array::from_fn(|i| Jet {
456                value: self.q[i].into(),
457                derivative: [if fixed_q {
458                    Complex::default()
459                } else {
460                    q[i].into()
461                }],
462            }),
463            self.axis,
464        )?;
465        let lhs = InterfaceMatrix::from_fn(|i, j| lhs[i][j].derivative[0]);
466        let rhs = InterfaceMatrix::from_fn(|i, j| rhs[i][j].derivative[0]);
467        let tangent = interface_blocks(&(inverse * (rhs - lhs * self.result)));
468        dimension(&tangent)?;
469        Ok(tangent)
470    }
471
472    /// Reuse the 4-by-4 inverse for the implicit solve adjoint; recompute local field derivatives.
473    pub fn pullback(&self, cotangent: &Blocks) -> Result<InterfaceGradient> {
474        // Jet seed offsets: `ks[i][j]` at `KS + 2 i + j`, `zs[i]` at `ZS + i` and `q[i]`
475        // at `Q + i`.
476        const KS: usize = 0;
477        const ZS: usize = 4;
478        const Q: usize = 6;
479        checked_dimension(cotangent, 2, "interface cotangent blocks must be 2 by 2")?;
480        let cotangent = interface_matrix(cotangent);
481        let inverse = self.inverse.ok_or_else(|| {
482            Error::InvalidInput(
483                "interface derivative is undefined at an exact diffraction threshold".into(),
484            )
485        })?;
486        let adjoint = inverse.adjoint() * cotangent;
487        let operator = -adjoint * self.result.adjoint();
488        let ks = std::array::from_fn(|i| {
489            std::array::from_fn(|j| Jet::<8>::variable(self.ks[i][j], KS + 2 * i + j))
490        });
491        let zs = std::array::from_fn(|i| Jet::variable(self.zs[i], ZS + i));
492        // At normal incidence the xy-interface blocks depend on q only to second
493        // order. Use their zero first derivative instead of an undefined
494        // azimuth derivative in the intermediate polarization vectors.
495        let fixed_q = self.fixed_q || (self.axis == 2 && self.q.iter().all(|&q| q == 0.0));
496        let q = std::array::from_fn(|i| {
497            if fixed_q {
498                Jet::constant(self.q[i])
499            } else {
500                Jet::variable(self.q[i], Q + i)
501            }
502        });
503        let (lhs, rhs) = interface_boundary(ks, zs, q, self.axis)?;
504        let mut result = [Complex::default(); 8];
505        for i in 0..4 {
506            for j in 0..4 {
507                for (a, g) in result.iter_mut().enumerate() {
508                    *g += operator[(i, j)] * lhs[i][j].derivative[a].conj()
509                        + adjoint[(i, j)] * rhs[i][j].derivative[a].conj();
510                }
511            }
512        }
513        Ok(InterfaceGradient {
514            ks: std::array::from_fn(|i| std::array::from_fn(|j| result[KS + 2 * i + j])),
515            z: [result[ZS], result[ZS + 1]],
516            q: [result[Q].re, result[Q + 1].re],
517        })
518    }
519}
520
521/// What [`propagation`] saves for its pullback: the wavevectors, the displacement and
522/// the diagonal phases.
523#[derive(Debug)]
524pub struct PropagationResidual {
525    vectors: Vec<[Complex; 3]>,
526    distance: [f64; 3],
527    /// Upward and downward transmission phase of every mode.
528    phases: Vec<[Complex; 2]>,
529}
530
531/// Propagate by a Cartesian displacement, given upgoing wavevectors for every mode.
532///
533/// The blocks are diagonal transmission phases and zero reflections.
534///
535/// Upstream: `treams.SMatrices.propagation`.
536pub fn propagation(
537    vectors: Vec<[Complex; 3]>,
538    distance: [f64; 3],
539) -> Result<(Blocks, PropagationResidual)> {
540    if vectors.is_empty()
541        || vectors.iter().flatten().any(|&v| !finite(v))
542        || distance.iter().any(|r| !r.is_finite())
543    {
544        return Err(Error::InvalidInput(
545            "propagation requires finite nonempty wavevectors and displacement".into(),
546        ));
547    }
548    let phases = vectors
549        .iter()
550        .map(|k| {
551            [1.0, -1.0].map(|sign| {
552                (Complex::i()
553                    * (sign * (k[0] * distance[0] + k[1] * distance[1]) + k[2] * distance[2]))
554                    .exp()
555            })
556        })
557        .collect::<Vec<_>>();
558    // Evanescent waves displaced against their decay overflow.
559    if phases.iter().flatten().any(|&phase| !finite(phase)) {
560        return Err(Error::NonFinite("non-finite propagation phase".into()));
561    }
562    let n = vectors.len();
563    let mut value: Blocks = std::array::from_fn(|_| DMatrix::zeros(n, n));
564    for (i, phase) in phases.iter().enumerate() {
565        value[0][(i, i)] = phase[0];
566        value[3][(i, i)] = phase[1];
567    }
568    Ok((
569        value,
570        PropagationResidual {
571            vectors,
572            distance,
573            phases,
574        },
575    ))
576}
577
578/// Cotangents of the inputs of [`propagation`], in the order of its arguments.
579#[derive(Clone, Debug)]
580pub struct PropagationGradient {
581    /// Complex wavevector cotangents, one per mode.
582    pub vectors: Vec<[Complex; 3]>,
583    /// Real displacement cotangent.
584    pub distance: [f64; 3],
585}
586
587impl PropagationResidual {
588    /// The shape `(n, n)` of each scattering block, with `n` modes in each direction.
589    #[must_use]
590    pub fn shape(&self) -> (usize, usize) {
591        (self.vectors.len(), self.vectors.len())
592    }
593
594    /// Propagate a wavevector and displacement direction through the saved phases.
595    pub fn pushforward(&self, vectors: &[[Complex; 3]], distance: [f64; 3]) -> Result<Blocks> {
596        if vectors.len() != self.vectors.len()
597            || vectors.iter().flatten().any(|&v| !finite(v))
598            || distance.iter().any(|v| !v.is_finite())
599        {
600            return Err(Error::InvalidInput(
601                "propagation tangents must be finite and match inputs".into(),
602            ));
603        }
604        let n = self.vectors.len();
605        let mut tangent: Blocks = std::array::from_fn(|_| DMatrix::zeros(n, n));
606        for (i, ((k, dk), phase)) in self
607            .vectors
608            .iter()
609            .zip(vectors)
610            .zip(&self.phases)
611            .enumerate()
612        {
613            let dot: [Complex; 3] =
614                std::array::from_fn(|a| dk[a] * self.distance[a] + k[a] * distance[a]);
615            for ((block, sign), phase) in [(0, 1.0), (3, -1.0)].into_iter().zip(phase) {
616                tangent[block][(i, i)] = Complex::i() * phase * (sign * (dot[0] + dot[1]) + dot[2]);
617            }
618        }
619        dimension(&tangent)?;
620        Ok(tangent)
621    }
622
623    /// Wavevector and displacement gradients from the cotangents of the four blocks.
624    pub fn pullback(&self, cotangent: &Blocks) -> Result<PropagationGradient> {
625        let n = checked_dimension(
626            cotangent,
627            self.vectors.len(),
628            "propagation cotangent shape mismatch",
629        )?;
630        let mut vectors = vec![[Complex::default(); 3]; n];
631        let mut distance = [0.0; 3];
632        for (i, (k, phase)) in self.vectors.iter().zip(&self.phases).enumerate() {
633            for ((block, sign), phase) in [(0, 1.0), (3, -1.0)].into_iter().zip(phase) {
634                let weight = cotangent[block][(i, i)].conj() * Complex::i() * phase;
635                for (a, s) in [sign, sign, 1.0].into_iter().enumerate() {
636                    vectors[i][a] += (weight * s * self.distance[a]).conj();
637                    distance[a] += (weight * s * k[a]).re;
638                }
639            }
640        }
641        Ok(PropagationGradient { vectors, distance })
642    }
643}