treams_core/smatrix/
tr.rs

1//! Transmittance and reflectance of plane-wave S-matrix columns between two ports.
2//!
3//! Upstream: `treams.SMatrices.tr`.
4
5mod saved;
6
7use faer::MatRef;
8use nalgebra::DMatrix;
9
10use super::{Blocks, StoredBlock, any_nonfinite, checked_dimension, interface::port_boundary};
11use crate::{
12    Complex, Error, Result,
13    linalg::{product_adjoint_right, product_views, view},
14    numerics::{Jet, finite, label_bits},
15};
16
17/// Jet seed offsets of the parameters: the wavenumbers `ks[port][pol]` at
18/// `KS + 2 port + pol`, the impedances `zs[port]` at `ZS + port` and the transverse
19/// components `q[j]` at `Q + j`.
20const KS: usize = 0;
21/// See [`KS`].
22const ZS: usize = 4;
23/// See [`KS`].
24const Q: usize = 6;
25
26/// Tangential fields `[E_a, E_b, H_a, H_b]` of the plane waves of one diffraction
27/// group, indexed by port, propagation direction and polarization.
28type PortWaves<const N: usize> = [[[[Jet<N>; 4]; 2]; 2]; 2];
29
30/// The port fields of one diffraction group, as jets in the eight parameters when
31/// `N = 8`; parity bases combine the helicity fields.
32fn port_waves<const N: usize>(
33    ks: [[Complex; 2]; 2],
34    zs: [Complex; 2],
35    q: [f64; 2],
36    axis: usize,
37    helicity: bool,
38    fixed_q: bool,
39) -> Result<PortWaves<N>> {
40    let ks: [[Jet<N>; 2]; 2] = std::array::from_fn(|port| {
41        std::array::from_fn(|pol| Jet::variable(ks[port][pol], KS + 2 * port + pol))
42    });
43    let zs: [Jet<N>; 2] = std::array::from_fn(|port| Jet::variable(zs[port], ZS + port));
44    let q = std::array::from_fn(|j| {
45        if fixed_q {
46            Jet::constant(q[j])
47        } else {
48            Jet::variable(q[j], Q + j)
49        }
50    });
51    port_wave_inputs(ks, zs, q, axis, helicity)
52}
53
54/// Field algebra shared by coordinate derivatives and a single directional seed.
55fn port_wave_inputs<const N: usize>(
56    ks: [[Jet<N>; 2]; 2],
57    zs: [Jet<N>; 2],
58    q: [Jet<N>; 2],
59    axis: usize,
60    helicity: bool,
61) -> Result<PortWaves<N>> {
62    let mut waves = [
63        port_boundary(ks[0], zs[0], q, axis)?,
64        port_boundary(ks[1], zs[1], q, axis)?,
65    ];
66    if !helicity {
67        for port in &mut waves {
68            for polarizations in port {
69                let [minus, plus] = *polarizations;
70                *polarizations = [
71                    std::array::from_fn(|j| (plus[j] - minus[j]) * std::f64::consts::FRAC_1_SQRT_2),
72                    std::array::from_fn(|j| (plus[j] + minus[j]) * std::f64::consts::FRAC_1_SQRT_2),
73                ];
74            }
75        }
76    }
77    Ok(waves)
78}
79
80/// The time-averaged power flux `Re(E x H*) / 2` along the normal of tangential
81/// fields `e` and `h`.
82fn cross(e: [Complex; 4], h: [Complex; 4]) -> f64 {
83    0.5 * (e[0] * h[3].conj() - e[1] * h[2].conj()).re
84}
85
86/// Real directional derivative of the sesquilinear power flux.
87fn cross_direction(e: [Jet<1>; 4], h: [Jet<1>; 4]) -> f64 {
88    cross(e.map(|v| v.derivative[0]), h.map(|v| v.value))
89        + cross(e.map(|v| v.value), h.map(|v| v.derivative[0]))
90}
91
92/// The cotangents of `e` and `h` in [`cross`] for the flux cotangent `weight`.
93fn cross_pullback(e: [Complex; 4], h: [Complex; 4], weight: f64) -> ([Complex; 4], [Complex; 4]) {
94    (
95        [
96            0.5 * weight * h[3],
97            -0.5 * weight * h[2],
98            Complex::default(),
99            Complex::default(),
100        ],
101        [
102            Complex::default(),
103            Complex::default(),
104            -0.5 * weight * e[1],
105            0.5 * weight * e[0],
106        ],
107    )
108}
109
110/// The incident, transmitted and reflected tangential fields of one illumination
111/// column, summed over the modes of each diffraction group.
112///
113/// `direction` is the propagation direction of the illumination; it also indexes the
114/// port that receives the transmitted wave.
115fn aggregate<const N: usize>(
116    waves: &[PortWaves<N>],
117    modes: &[(usize, u8)],
118    amplitudes: impl Fn(usize) -> [Jet<N>; 3],
119    direction: usize,
120) -> Vec<[[Jet<N>; 4]; 3]> {
121    let transmission = direction;
122    let reflection = 1 - transmission;
123    let mut fields = vec![[[Jet::default(); 4]; 3]; waves.len()];
124    for (mode, &(group, pol)) in modes.iter().enumerate() {
125        for (which, ((port, wave_direction), amplitude)) in [
126            (reflection, transmission),
127            (transmission, transmission),
128            (reflection, reflection),
129        ]
130        .into_iter()
131        .zip(amplitudes(mode))
132        .enumerate()
133        {
134            for (j, field) in fields[group][which].iter_mut().enumerate() {
135                *field += waves[group][port][wave_direction][usize::from(pol)][j] * amplitude;
136            }
137        }
138    }
139    fields
140}
141
142/// Incident, transmitted and reflected amplitudes of one mode and illumination.
143fn amplitudes(
144    incident: &DMatrix<Complex>,
145    outgoing: &[DMatrix<Complex>; 2],
146    mode: usize,
147    illumination: usize,
148) -> [Complex; 3] {
149    [
150        incident[(mode, illumination)],
151        outgoing[0][(mode, illumination)],
152        outgoing[1][(mode, illumination)],
153    ]
154}
155
156/// The two plane-wave ports of [`tr`] and [`tr_value`]: port 0 on the positive side
157/// (above), port 1 on the negative side (below).
158#[derive(Clone, Debug)]
159pub struct TrPorts {
160    /// Wavenumbers of polarizations 0 and 1 in the medium of each port.
161    pub ks: [[Complex; 2]; 2],
162    /// Impedance of the medium of each port.
163    pub zs: [Complex; 2],
164    /// One real transverse wavevector per distinct diffraction group.
165    pub q: Vec<[f64; 2]>,
166    /// The distinct (diffraction group, polarization) pair of every mode.
167    pub modes: Vec<(usize, u8)>,
168    /// Cartesian normal axis.
169    pub axis: usize,
170    /// Helicity polarizations; parity (TE/TM) otherwise.
171    pub helicity: bool,
172    /// Propagation direction of the illumination: 0 = up (towards the positive side,
173    /// incident from the negative side), 1 = down.
174    pub direction: usize,
175}
176
177impl TrPorts {
178    #[allow(clippy::float_cmp)] // Exact diffraction groups.
179    fn validate(&self) -> Result<()> {
180        if self.axis > 2
181            || self.direction > 1
182            || self
183                .modes
184                .iter()
185                .any(|&(group, pol)| group >= self.q.len() || pol > 1)
186            || self.q.iter().flatten().any(|v| !v.is_finite())
187            || self
188                .ks
189                .iter()
190                .flatten()
191                .chain(&self.zs)
192                .any(|&v| !finite(v) || v == Complex::default())
193        {
194            return Err(Error::InvalidInput(INVALID_INPUTS.into()));
195        }
196        if !self.helicity && self.ks.iter().any(|k| k[0] != k[1]) {
197            return Err(Error::InvalidInput(
198                "parity power requires achiral port media".into(),
199            ));
200        }
201        let mut unique = std::collections::HashSet::new();
202        if self.q.iter().any(|q| !unique.insert(q.map(label_bits))) {
203            return Err(Error::InvalidInput(
204                "transverse groups must be distinct; place coherent modes in the same group".into(),
205            ));
206        }
207        // A repeated mode would add its field twice to the flux and the powers.
208        let mut unique = std::collections::HashSet::new();
209        if self.modes.iter().any(|&mode| !unique.insert(mode)) {
210            return Err(Error::InvalidInput("plane modes must be distinct".into()));
211        }
212        Ok(())
213    }
214
215    /// Tangential port fields of one diffraction group.
216    fn waves<const N: usize>(&self, q: [f64; 2], fixed_q: bool) -> Result<PortWaves<N>> {
217        port_waves(self.ks, self.zs, q, self.axis, self.helicity, fixed_q)
218    }
219}
220
221const INVALID_INPUTS: &str = "require matching finite square S blocks, illumination columns, port media and valid plane modes";
222
223/// What [`tr`] saves for its pullback: copies of the transmission and reflection blocks
224/// of the illumination direction, the incident fields, the ports and the forward
225/// results.
226#[derive(Debug)]
227pub struct TrResidual {
228    matrices: [StoredBlock; 2],
229    incident: DMatrix<Complex>,
230    ports: TrPorts,
231    forward: TrForward,
232    fixed_q: bool,
233}
234
235/// S-matrix, illumination, port wavenumber/impedance and transverse-wavevector cotangents.
236#[derive(Debug)]
237pub struct TrGradient {
238    /// Cotangents of the four scattering blocks.
239    pub matrices: Blocks,
240    /// Cotangent of the illumination, one column per independent illumination.
241    pub incident: DMatrix<Complex>,
242    /// Cotangents of the port 0/1, helicity 0/1 wavenumbers.
243    pub ks: [[Complex; 2]; 2],
244    /// Cotangents of the port impedances.
245    pub zs: [Complex; 2],
246    /// Cotangents of the transverse vectors, one per distinct diffraction direction;
247    /// zero when the forward holds `q` fixed.
248    pub q: Vec<[f64; 2]>,
249}
250
251/// Forward values with the intermediates the pullback reuses.
252#[derive(Debug)]
253struct TrForward {
254    /// Rows are transmittance/reflectance; columns are independent illuminations.
255    value: DMatrix<f64>,
256    flux: Vec<f64>,
257    outgoing: [DMatrix<Complex>; 2],
258    waves: Vec<PortWaves<0>>,
259}
260
261/// Validate the inputs and compute the transmittance and reflectance.
262fn evaluate(
263    matrices: [MatRef<'_, Complex>; 2],
264    incident: &DMatrix<Complex>,
265    ports: &TrPorts,
266) -> Result<TrForward> {
267    let n = ports.modes.len();
268    if n == 0
269        || incident.nrows() != n
270        || incident.ncols() == 0
271        || matrices.iter().any(|m| m.nrows() != n || m.ncols() != n)
272        || incident.iter().any(|&z| !finite(z))
273        || matrices.iter().any(|&m| any_nonfinite(m))
274    {
275        return Err(Error::InvalidInput(INVALID_INPUTS.into()));
276    }
277    ports.validate()?;
278    let waves = ports
279        .q
280        .iter()
281        .map(|&q| ports.waves::<0>(q, true))
282        .collect::<Result<Vec<_>>>()?;
283    let outgoing = matrices.map(|m| product_views(m, view(incident)));
284    let sign = if ports.direction == 0 { 1.0 } else { -1.0 };
285    let mut flux = vec![0.0; incident.ncols()];
286    let mut value = DMatrix::zeros(2, incident.ncols());
287    for illumination in 0..incident.ncols() {
288        let mut transmitted = 0.0;
289        let mut reflected = 0.0;
290        for [input, trans, reflect] in aggregate(
291            &waves,
292            &ports.modes,
293            |mode| amplitudes(incident, &outgoing, mode, illumination).map(Jet::constant),
294            ports.direction,
295        ) {
296            let [input, trans, reflect] = [input, trans, reflect].map(|f| f.map(|v| v.value));
297            flux[illumination] +=
298                sign * (cross(input, input) + cross(input, reflect) + cross(reflect, input));
299            transmitted += sign * cross(trans, trans);
300            reflected -= sign * cross(reflect, reflect);
301        }
302        if !flux[illumination].is_finite() || flux[illumination] <= 0.0 {
303            return Err(Error::InvalidInput(
304                "transmittance requires positive finite incident power flux".into(),
305            ));
306        }
307        value[(0, illumination)] = transmitted / flux[illumination];
308        value[(1, illumination)] = reflected / flux[illumination];
309    }
310    if value.iter().any(|v| !v.is_finite()) {
311        return Err(Error::NonFinite(
312            "non-finite transmission or reflection".into(),
313        ));
314    }
315    Ok(TrForward {
316        value,
317        flux,
318        outgoing,
319        waves,
320    })
321}
322
323/// The transmittance and reflectance of [`tr`] without the data its pullback needs.
324/// Matrix views are the transmission and reflection blocks for the illumination
325/// direction.
326///
327/// Upstream: `treams.SMatrices.tr`.
328#[doc(alias = "transmittance")]
329#[doc(alias = "power")]
330pub fn tr_value(
331    matrices: [MatRef<'_, Complex>; 2],
332    incident: &DMatrix<Complex>,
333    ports: &TrPorts,
334) -> Result<DMatrix<f64>> {
335    Ok(evaluate(matrices, incident, ports)?.value)
336}
337
338/// Transmittance and reflectance of S-matrix columns between two plane-wave ports.
339///
340/// [`TrResidual::value`] has the rows (transmittance, reflectance) and one column per
341/// illumination; the residual keeps it because its pullback reads it. The pullback
342/// includes the interference of incident and reflected waves in a lossy medium. With
343/// `fixed_q`, it holds the transverse wavevectors fixed and supports exactly normal
344/// incidence; otherwise derivatives preserve the distinct diffraction-group topology.
345///
346/// Upstream: `treams.SMatrices.tr`.
347#[doc(alias = "transmittance")]
348#[doc(alias = "power")]
349pub fn tr(
350    matrices: [MatRef<'_, Complex>; 2],
351    incident: DMatrix<Complex>,
352    ports: TrPorts,
353    fixed_q: bool,
354) -> Result<TrResidual> {
355    let forward = evaluate(matrices, &incident, &ports)?;
356    Ok(TrResidual {
357        matrices: matrices.map(StoredBlock::copy_view),
358        incident,
359        ports,
360        forward,
361        fixed_q,
362    })
363}
364
365impl TrResidual {
366    /// Transmittance and reflectance: the rows, with one column per illumination. The
367    /// pullback reads them.
368    #[must_use]
369    pub const fn value(&self) -> &DMatrix<f64> {
370        &self.forward.value
371    }
372
373    /// Rows (transmittance, reflectance) and independent illuminations.
374    #[must_use]
375    pub fn shape(&self) -> (usize, usize) {
376        self.forward.value.shape()
377    }
378
379    /// Numbers of modes, independent illumination columns and diffraction groups.
380    #[must_use]
381    pub fn input_shape(&self) -> (usize, usize, usize) {
382        (
383            self.incident.nrows(),
384            self.incident.ncols(),
385            self.ports.q.len(),
386        )
387    }
388
389    /// Propagate matrix, illumination and port-geometry directions together. Local
390    /// one-direction jets include the real power pairing and lossy-port interference.
391    pub fn pushforward(
392        &self,
393        matrices: &Blocks,
394        incident: &DMatrix<Complex>,
395        ks: [[Complex; 2]; 2],
396        zs: [Complex; 2],
397        q: &[[f64; 2]],
398    ) -> Result<DMatrix<f64>> {
399        checked_dimension(
400            matrices,
401            self.incident.nrows(),
402            "power matrix tangent shape mismatch",
403        )?;
404        if incident.shape() != self.incident.shape()
405            || q.len() != self.ports.q.len()
406            || incident
407                .iter()
408                .chain(ks.iter().flatten())
409                .chain(&zs)
410                .any(|&v| !finite(v))
411            || q.iter().flatten().any(|v| !v.is_finite())
412        {
413            return Err(Error::InvalidInput(
414                "power tangents must be finite and match inputs".into(),
415            ));
416        }
417        let t = self.ports.direction;
418        let r = 1 - t;
419        let outgoing = std::array::from_fn(|which| {
420            let direction = if which == 0 { t } else { r };
421            product_views(view(&matrices[2 * direction + t]), view(&self.incident))
422                + product_views(self.matrices[which].view(), view(incident))
423        });
424        let fixed_media = ks
425            .iter()
426            .flatten()
427            .chain(&zs)
428            .all(|&v| v == Complex::default());
429        let ks = std::array::from_fn(|p| {
430            std::array::from_fn(|j| Jet {
431                value: self.ports.ks[p][j],
432                derivative: [ks[p][j]],
433            })
434        });
435        let zs = std::array::from_fn(|p| Jet {
436            value: self.ports.zs[p],
437            derivative: [zs[p]],
438        });
439        let waves = self
440            .ports
441            .q
442            .iter()
443            .zip(q)
444            .enumerate()
445            .map(|(group, (value, tangent))| {
446                // Matrix/illumination sensitivities reuse the saved port fields;
447                // they do not require derivatives of the port dispersion.
448                if fixed_media && (self.fixed_q || tangent.iter().all(|&v| v == 0.0)) {
449                    return Ok(self.forward.waves[group].map(|port| {
450                        port.map(|direction| {
451                            direction.map(|wave| wave.map(|v| Jet::constant(v.value)))
452                        })
453                    }));
454                }
455                port_wave_inputs(
456                    ks,
457                    zs,
458                    std::array::from_fn(|j| Jet {
459                        value: value[j].into(),
460                        derivative: [if self.fixed_q {
461                            Complex::default()
462                        } else {
463                            tangent[j].into()
464                        }],
465                    }),
466                    self.ports.axis,
467                    self.ports.helicity,
468                )
469            })
470            .collect::<Result<Vec<_>>>()?;
471        let sign = if t == 0 { 1.0 } else { -1.0 };
472        let mut tangent = DMatrix::zeros(2, incident.ncols());
473        for illumination in 0..incident.ncols() {
474            let fields = aggregate(
475                &waves,
476                &self.ports.modes,
477                |mode| {
478                    let values =
479                        amplitudes(&self.incident, &self.forward.outgoing, mode, illumination);
480                    let directions = amplitudes(incident, &outgoing, mode, illumination);
481                    std::array::from_fn(|i| Jet {
482                        value: values[i],
483                        derivative: [directions[i]],
484                    })
485                },
486                t,
487            );
488            let mut dflux = 0.0;
489            let mut dtransmitted = 0.0;
490            let mut dreflected = 0.0;
491            for [input, trans, reflect] in fields {
492                dflux += sign
493                    * (cross_direction(input, input)
494                        + cross_direction(input, reflect)
495                        + cross_direction(reflect, input));
496                dtransmitted += sign * cross_direction(trans, trans);
497                dreflected -= sign * cross_direction(reflect, reflect);
498            }
499            for (row, dpower) in [dtransmitted, dreflected].into_iter().enumerate() {
500                tangent[(row, illumination)] = (dpower
501                    - self.forward.value[(row, illumination)] * dflux)
502                    / self.forward.flux[illumination];
503            }
504        }
505        if tangent.iter().any(|v| !v.is_finite()) {
506            return Err(Error::NonFinite("non-finite power derivative".into()));
507        }
508        Ok(tangent)
509    }
510
511    /// Pull back the cotangents of the transmittance and reflectance.
512    pub fn pullback(&self, cotangent: &DMatrix<f64>) -> Result<TrGradient> {
513        if cotangent.shape() != self.shape() || cotangent.iter().any(|v| !v.is_finite()) {
514            return Err(Error::InvalidInput(
515                "power cotangent must be finite and match output".into(),
516            ));
517        }
518        let Self {
519            matrices: stored,
520            incident: input,
521            ports,
522            forward:
523                TrForward {
524                    value,
525                    flux,
526                    outgoing: output,
527                    waves,
528                },
529            fixed_q,
530        } = self;
531        let n = ports.modes.len();
532        let t = ports.direction;
533        let r = 1 - t;
534        let sign = if t == 0 { 1.0 } else { -1.0 };
535        let mut gwaves = vec![[[[[Complex::default(); 4]; 2]; 2]; 2]; ports.q.len()];
536        let mut incident = DMatrix::zeros(n, input.ncols());
537        let mut outgoing = [incident.clone(), incident.clone()];
538        for illumination in 0..input.ncols() {
539            let wt = cotangent[(0, illumination)] / flux[illumination];
540            let wr = cotangent[(1, illumination)] / flux[illumination];
541            let wi = -(cotangent[(0, illumination)] * value[(0, illumination)]
542                + cotangent[(1, illumination)] * value[(1, illumination)])
543                / flux[illumination];
544            let fields = aggregate(
545                waves,
546                &ports.modes,
547                |mode| amplitudes(input, output, mode, illumination).map(Jet::constant),
548                t,
549            );
550            let mut gradient = vec![[[Complex::default(); 4]; 3]; ports.q.len()];
551            for (group, f) in fields.iter().enumerate() {
552                let f = f.map(|field| field.map(|v| v.value));
553                for (e, h, weight) in [
554                    (0, 0, sign * wi),
555                    (0, 2, sign * wi),
556                    (2, 0, sign * wi),
557                    (1, 1, sign * wt),
558                    (2, 2, -sign * wr),
559                ] {
560                    let (ge, gh) = cross_pullback(f[e], f[h], weight);
561                    for j in 0..4 {
562                        gradient[group][e][j] += ge[j];
563                        gradient[group][h][j] += gh[j];
564                    }
565                }
566            }
567            for (mode, &(group, pol)) in ports.modes.iter().enumerate() {
568                let amplitudes = [
569                    input[(mode, illumination)],
570                    output[0][(mode, illumination)],
571                    output[1][(mode, illumination)],
572                ];
573                for (which, (port, wave_direction)) in
574                    [(r, t), (t, t), (r, r)].into_iter().enumerate()
575                {
576                    let mut amplitude_gradient = Complex::default();
577                    for j in 0..4 {
578                        let g = gradient[group][which][j];
579                        amplitude_gradient += waves[group][port][wave_direction][usize::from(pol)]
580                            [j]
581                            .value
582                            .conj()
583                            * g;
584                        gwaves[group][port][wave_direction][usize::from(pol)][j] +=
585                            amplitudes[which].conj() * g;
586                    }
587                    if which == 0 {
588                        incident[(mode, illumination)] += amplitude_gradient;
589                    } else {
590                        outgoing[which - 1][(mode, illumination)] += amplitude_gradient;
591                    }
592                }
593            }
594        }
595        let mut matrices = std::array::from_fn(|_| DMatrix::zeros(n, n));
596        for (which, direction) in [t, r].into_iter().enumerate() {
597            matrices[2 * direction + t] = product_adjoint_right(&outgoing[which], input);
598            incident += product_views(stored[which].view().adjoint(), view(&outgoing[which]));
599        }
600        let mut result = TrGradient {
601            matrices,
602            incident,
603            ks: [[Complex::default(); 2]; 2],
604            zs: [Complex::default(); 2],
605            q: vec![[0.0; 2]; ports.q.len()],
606        };
607        for (group, &q) in ports.q.iter().enumerate() {
608            if gwaves[group]
609                .iter()
610                .flatten()
611                .flatten()
612                .flatten()
613                .all(|&v| v == Complex::default())
614            {
615                continue;
616            }
617            let jets = ports.waves::<8>(q, *fixed_q)?;
618            let mut parameters = [Complex::default(); 8];
619            for (v, g) in jets
620                .iter()
621                .flatten()
622                .flatten()
623                .flatten()
624                .zip(gwaves[group].iter().flatten().flatten().flatten())
625            {
626                for (target, d) in parameters.iter_mut().zip(v.derivative) {
627                    *target += g * d.conj();
628                }
629            }
630            for port in 0..2 {
631                for pol in 0..2 {
632                    result.ks[port][pol] += parameters[KS + 2 * port + pol];
633                }
634                result.zs[port] += parameters[ZS + port];
635            }
636            result.q[group] = [parameters[Q].re, parameters[Q + 1].re];
637        }
638        Ok(result)
639    }
640}