treams_core/pw/
field.rs

1//! Plane-wave translation phases and fields, with analytic pullbacks.
2//!
3//! Upstream: `treams.pw.translate` and `treams.special.vpw_M`, `vpw_N` and `vpw_A`.
4#![allow(clippy::indexing_slicing)] // Validated three-component vectors and polarizations.
5
6use nalgebra::DMatrix;
7
8use super::polarization::{polarization, polarization_jet};
9use crate::{
10    Complex, Error, Result,
11    numerics::{
12        self, Jet, finite,
13        parallel::{PARALLEL_ENTRIES, try_fill_chunks, try_fold_ordered, try_map},
14    },
15};
16
17/// `exp(i k·r)` for a complex wavevector `k` and a real point `r`, from the real and
18/// imaginary parts of `k·r` separately.
19#[inline]
20pub(crate) fn phase(vector: [Complex; 3], point: [f64; 3]) -> Complex {
21    let angle = vector[0].re * point[0] + vector[1].re * point[1] + vector[2].re * point[2];
22    let attenuation = vector[0].im * point[0] + vector[1].im * point[1] + vector[2].im * point[2];
23    let (sine, cosine) = angle.sin_cos();
24    let scale = (-attenuation).exp();
25    Complex::new(scale * cosine, scale * sine)
26}
27
28/// What [`phases`] saves for its pullback: the points and the wavevectors, without the
29/// phases of every point and mode.
30#[derive(Debug)]
31pub struct PhasesResidual {
32    pub(super) points: Vec<[f64; 3]>,
33    pub(super) vectors: Vec<[Complex; 3]>,
34}
35/// Cotangents of real displacements and complex full wavevectors.
36#[derive(Debug)]
37pub struct PhasesGradient {
38    /// Real displacement cotangents.
39    pub points: Vec<[f64; 3]>,
40    /// Complex wavevector cotangents in the real Hermitian pairing.
41    pub vectors: Vec<[Complex; 3]>,
42}
43
44/// Direct scalar plane translation phase.
45///
46/// Upstream: `treams.pw.translate`.
47#[inline]
48pub fn translate(vector: [Complex; 3], point: [f64; 3]) -> Result<Complex> {
49    if vector.iter().any(|&v| !finite(v)) || point.iter().any(|v| !v.is_finite()) {
50        return Err(Error::InvalidInput(
51            "plane translation requires finite arguments".into(),
52        ));
53    }
54    let value = phase(vector, point);
55    if !finite(value) {
56        return Err(Error::NonFinite("non-finite plane translation".into()));
57    }
58    Ok(value)
59}
60
61/// Plane-wave translation phases with shape (displacements, wavevectors).
62///
63/// Upstream: `treams.pw.translate` on arrays of points and wavevectors.
64pub fn phases(
65    points: Vec<[f64; 3]>,
66    vectors: Vec<[Complex; 3]>,
67) -> Result<(DMatrix<Complex>, PhasesResidual)> {
68    if vectors.is_empty()
69        || vectors.iter().flatten().any(|&k| !finite(k))
70        || points.iter().flatten().any(|r| !r.is_finite())
71    {
72        return Err(Error::InvalidInput(
73            "require finite displacements and nonempty finite wavevectors".into(),
74        ));
75    }
76    let mut value = DMatrix::zeros(points.len(), vectors.len());
77    let parallel = value.len() >= PARALLEL_ENTRIES;
78    try_fill_chunks(
79        value.as_mut_slice(),
80        points.len().max(1),
81        parallel,
82        |j, column| -> Result<()> {
83            for (out, &point) in column.iter_mut().zip(&points) {
84                *out = phase(vectors[j], point);
85            }
86            Ok(())
87        },
88    )?;
89    if value.iter().any(|&v| !finite(v)) {
90        return Err(Error::NonFinite("plane translation phase overflow".into()));
91    }
92    Ok((value, PhasesResidual { points, vectors }))
93}
94impl PhasesResidual {
95    /// Number of displacements and wavevectors.
96    #[must_use]
97    pub fn shape(&self) -> (usize, usize) {
98        (self.points.len(), self.vectors.len())
99    }
100
101    /// Validate point and wavevector tangents for the recorded operation.
102    pub fn validate_tangents(&self, points: &[[f64; 3]], vectors: &[[Complex; 3]]) -> Result<()> {
103        if points.len() != self.points.len()
104            || vectors.len() != self.vectors.len()
105            || points.iter().flatten().any(|r| !r.is_finite())
106            || vectors.iter().flatten().any(|&k| !finite(k))
107        {
108            return Err(Error::InvalidInput("invalid plane-phase tangents".into()));
109        }
110        Ok(())
111    }
112
113    /// Differentiate each phase in one input direction without constructing a Jacobian.
114    pub fn pushforward(
115        &self,
116        points: &[[f64; 3]],
117        vectors: &[[Complex; 3]],
118    ) -> Result<DMatrix<Complex>> {
119        self.validate_tangents(points, vectors)?;
120        let mut tangent = DMatrix::zeros(self.points.len(), self.vectors.len());
121        let parallel = tangent.len() >= PARALLEL_ENTRIES;
122        try_fill_chunks(
123            tangent.as_mut_slice(),
124            self.points.len().max(1),
125            parallel,
126            |j, column| -> Result<()> {
127                for (i, out) in column.iter_mut().enumerate() {
128                    let angle: Complex = (0..3)
129                        .map(|a| {
130                            self.vectors[j][a] * points[i][a] + vectors[j][a] * self.points[i][a]
131                        })
132                        .sum();
133                    *out = Complex::i() * phase(self.vectors[j], self.points[i]) * angle;
134                }
135                Ok(())
136            },
137        )?;
138        Ok(tangent)
139    }
140    /// Recompute each phase once and add its terms to the point and wavevector gradients.
141    ///
142    /// The longer axis is split into at most 32 blocks, fixed by the shape, that
143    /// run in parallel; each block sums its own rows (or columns) in index order and
144    /// a partial gradient of the shorter axis, and the partials are added in block
145    /// order. The result is deterministic and the scratch is at most 32 gradients
146    /// of the shorter axis.
147    pub fn pullback<S: nalgebra::Storage<Complex, nalgebra::Dyn, nalgebra::Dyn> + Sync>(
148        &self,
149        cotangent: &nalgebra::Matrix<Complex, nalgebra::Dyn, nalgebra::Dyn, S>,
150    ) -> Result<PhasesGradient> {
151        if cotangent.shape() != self.shape() || cotangent.iter().any(|&v| !finite(v)) {
152            return Err(Error::InvalidInput("invalid plane-phase cotangent".into()));
153        }
154        let (rows, columns) = self.shape();
155        // Add the phase terms of points `i0..` and wavevectors `j0..` to gradients
156        // indexed from those offsets; each entry sums over the other axis in order.
157        let contract =
158            |i0: usize, points: &mut [[f64; 3]], j0: usize, vectors: &mut [[Complex; 3]]| {
159                for (j, (vector_gradient, &vector)) in
160                    vectors.iter_mut().zip(&self.vectors[j0..]).enumerate()
161                {
162                    for (i, (point_gradient, &point)) in
163                        points.iter_mut().zip(&self.points[i0..]).enumerate()
164                    {
165                        let factor = cotangent[(i0 + i, j0 + j)].conj()
166                            * Complex::i()
167                            * phase(vector, point);
168                        for axis in 0..3 {
169                            point_gradient[axis] += (factor * vector[axis]).re;
170                            vector_gradient[axis] += factor.conj() * point[axis];
171                        }
172                    }
173                }
174            };
175        let long = rows.max(columns);
176        let block = long.div_ceil(32).max(16);
177        let blocks = long.div_ceil(block);
178        let task = |b: usize| {
179            let (start, stop) = (b * block, ((b + 1) * block).min(long));
180            if rows >= columns {
181                let mut points = vec![[0.0; 3]; stop - start];
182                let mut vectors = vec![[Complex::default(); 3]; columns];
183                contract(start, &mut points, 0, &mut vectors);
184                (points, vectors)
185            } else {
186                let mut points = vec![[0.0; 3]; rows];
187                let mut vectors = vec![[Complex::default(); 3]; stop - start];
188                contract(0, &mut points, start, &mut vectors);
189                (points, vectors)
190            }
191        };
192        let partials = try_map(
193            blocks,
194            cotangent.len() >= PARALLEL_ENTRIES,
195            |b| -> Result<_> { Ok(task(b)) },
196        )?;
197        let mut gradient = PhasesGradient {
198            points: Vec::with_capacity(rows),
199            vectors: Vec::with_capacity(columns),
200        };
201        if rows >= columns {
202            gradient.vectors = vec![[Complex::default(); 3]; columns];
203            for (points, vectors) in partials {
204                gradient.points.extend(points);
205                for (target, partial) in gradient
206                    .vectors
207                    .iter_mut()
208                    .flatten()
209                    .zip(vectors.iter().flatten())
210                {
211                    *target += partial;
212                }
213            }
214        } else {
215            gradient.points = vec![[0.0; 3]; rows];
216            for (points, vectors) in partials {
217                gradient.vectors.extend(vectors);
218                for (target, partial) in gradient
219                    .points
220                    .iter_mut()
221                    .flatten()
222                    .zip(points.iter().flatten())
223                {
224                    *target += partial;
225                }
226            }
227        }
228        Ok(gradient)
229    }
230}
231/// What [`field`] saves for its pullback: its inputs, for weighted fields or for the
232/// matrix that samples every mode.
233///
234/// The pullback splits the longer of the point and mode axes into at most 32 blocks,
235/// fixed by the shape, as [`PhasesResidual::pullback`] does. Each block writes the
236/// gradients of its own points or modes and a partial sum of the gradients of the
237/// other axis, and the partial sums add in block order; a mode's polarization pulls
238/// back once, from its total over all points. The blocks run in parallel from 4096
239/// points times modes (`PARALLEL_ENTRIES`), and in the same order on the calling thread
240/// below, so the thread count does not change the result.
241#[derive(Debug)]
242pub struct FieldResidual {
243    pub(super) vectors: Vec<[Complex; 3]>,
244    pub(super) polarizations: Vec<u8>,
245    pub(super) points: Vec<[f64; 3]>,
246    pub(super) coefficients: Option<Vec<Complex>>,
247    pub(super) helicity: bool,
248    pub(super) fixed_vectors: bool,
249}
250/// Cotangents in the real Hermitian pairing.
251#[derive(Debug)]
252pub struct FieldGradient {
253    /// Amplitude cotangents; empty for a field operator.
254    pub coefficients: Vec<Complex>,
255    /// Real sample-point cotangents.
256    pub points: Vec<[f64; 3]>,
257    /// Complex full-wavevector cotangents; zero when wavevectors are held fixed.
258    pub vectors: Vec<[Complex; 3]>,
259}
260/// Evaluate weighted plane fields, or the full operator when coefficients are absent.
261///
262/// The rows hold the three Cartesian components of each point in turn. With
263/// `fixed_vectors`, the pullback holds the wavevectors fixed and returns zero wavevector
264/// gradients.
265///
266/// Upstream: sums of `treams.special.vpw_M`, `vpw_N` or `vpw_A` over the plane modes.
267pub fn field(
268    vectors: Vec<[Complex; 3]>,
269    polarizations: Vec<u8>,
270    points: Vec<[f64; 3]>,
271    coefficients: Option<Vec<Complex>>,
272    helicity: bool,
273    fixed_vectors: bool,
274) -> Result<(DMatrix<Complex>, FieldResidual)> {
275    if vectors.is_empty()
276        || vectors.len() != polarizations.len()
277        || points.iter().flatten().any(|r| !r.is_finite())
278        || coefficients
279            .as_ref()
280            .is_some_and(|c| c.len() != vectors.len() || c.iter().any(|&v| !finite(v)))
281    {
282        return Err(Error::InvalidInput(
283            "require nonempty plane modes, finite points, matching finite amplitudes and polarizations"
284                .into(),
285        ));
286    }
287    let electric: Vec<_> = vectors
288        .iter()
289        .zip(&polarizations)
290        .map(|(&k, &p)| polarization(k, p, helicity))
291        .collect::<Result<_>>()?;
292    let mut value = numerics::zeros(
293        3 * points.len(),
294        if coefficients.is_some() {
295            1
296        } else {
297            vectors.len()
298        },
299    )?;
300    let parallel = points.len() * vectors.len() >= PARALLEL_ENTRIES;
301    if let Some(c) = &coefficients {
302        try_fill_chunks(value.as_mut_slice(), 3, parallel, |p, out| -> Result<()> {
303            for ((&k, e), &c) in vectors.iter().zip(&electric).zip(c) {
304                let weight = c * phase(k, points[p]);
305                for (out, &e) in out.iter_mut().zip(e) {
306                    *out += weight * e;
307                }
308            }
309            Ok(())
310        })?;
311    } else if !points.is_empty() {
312        let size = 3 * points.len();
313        try_fill_chunks(
314            value.as_mut_slice(),
315            size,
316            parallel,
317            |j, column| -> Result<()> {
318                for (out, &point) in column.as_chunks_mut::<3>().0.iter_mut().zip(&points) {
319                    let phase = phase(vectors[j], point);
320                    for (out, &e) in out.iter_mut().zip(&electric[j]) {
321                        *out = phase * e;
322                    }
323                }
324                Ok(())
325            },
326        )?;
327    }
328    if value.iter().any(|&v| !finite(v)) {
329        return Err(Error::NonFinite("plane field overflow".into()));
330    }
331    Ok((
332        value,
333        FieldResidual {
334            vectors,
335            polarizations,
336            points,
337            coefficients,
338            helicity,
339            fixed_vectors,
340        },
341    ))
342}
343impl FieldResidual {
344    /// Number of weighted amplitudes, or zero for a field operator.
345    #[must_use]
346    pub fn coefficient_count(&self) -> usize {
347        self.coefficients.as_ref().map_or(0, Vec::len)
348    }
349
350    /// Flattened output dimensions (3 * samples, modes or one weighted column).
351    #[must_use]
352    pub fn shape(&self) -> (usize, usize) {
353        (
354            3 * self.points.len(),
355            if self.coefficients.is_some() {
356                1
357            } else {
358                self.vectors.len()
359            },
360        )
361    }
362
363    /// Validate amplitude, sample-point and wavevector tangents. An operator takes
364    /// an empty amplitude tangent because it has no amplitude input.
365    pub fn validate_tangents(
366        &self,
367        coefficients: &[Complex],
368        points: &[[f64; 3]],
369        vectors: &[[Complex; 3]],
370    ) -> Result<()> {
371        if coefficients.len() != self.coefficient_count()
372            || points.len() != self.points.len()
373            || vectors.len() != self.vectors.len()
374            || coefficients.iter().any(|&c| !finite(c))
375            || points.iter().flatten().any(|r| !r.is_finite())
376            || vectors.iter().flatten().any(|&k| !finite(k))
377        {
378            return Err(Error::InvalidInput("invalid plane-field tangents".into()));
379        }
380        Ok(())
381    }
382
383    /// Push one direction through the amplitudes, phases and polarizations. Each
384    /// mode's polarization derivative is contracted once and reused at every point.
385    pub fn pushforward(
386        &self,
387        coefficients: &[Complex],
388        points: &[[f64; 3]],
389        vectors: &[[Complex; 3]],
390    ) -> Result<DMatrix<Complex>> {
391        self.validate_tangents(coefficients, points, vectors)?;
392        let (rows, columns) = self.shape();
393        let mut tangent = numerics::zeros(rows, columns)?;
394        if self.points.is_empty() {
395            return Ok(tangent);
396        }
397        let terms = (0..self.vectors.len())
398            .map(|j| {
399                let vector = if self.fixed_vectors {
400                    [Complex::default(); 3]
401                } else {
402                    vectors[j]
403                };
404                let mode = self.mode(j, vector.iter().any(|&v| v != Complex::default()))?;
405                let electric: [Complex; 3] = mode
406                    .electric
407                    .map(|e| e.derivative.iter().zip(vector).map(|(&d, v)| d * v).sum());
408                Ok((mode, vector, electric))
409            })
410            .collect::<Result<Vec<_>>>()?;
411        let parallel = self.points.len() * self.vectors.len() >= PARALLEL_ENTRIES;
412        // This local contraction is shared by the weighted and operator layouts.
413        let evaluate = |i: usize, j: usize| -> [Complex; 3] {
414            let (mode, vector, electric) = &terms[j];
415            let phase = phase(mode.vector, self.points[i]);
416            let angle: Complex = (0..3)
417                .map(|a| mode.vector[a] * points[i][a] + vector[a] * self.points[i][a])
418                .sum();
419            let coefficient = coefficients.get(j).copied().unwrap_or_default();
420            let weight = coefficient + Complex::i() * angle * mode.coefficient;
421            std::array::from_fn(|a| {
422                phase * (weight * mode.electric[a].value + mode.coefficient * electric[a])
423            })
424        };
425        if self.coefficients.is_some() {
426            try_fill_chunks(
427                tangent.as_mut_slice(),
428                3,
429                parallel,
430                |i, out| -> Result<()> {
431                    for j in 0..self.vectors.len() {
432                        for (out, value) in out.iter_mut().zip(evaluate(i, j)) {
433                            *out += value;
434                        }
435                    }
436                    Ok(())
437                },
438            )?;
439        } else {
440            try_fill_chunks(
441                tangent.as_mut_slice(),
442                rows.max(1),
443                parallel,
444                |j, out| -> Result<()> {
445                    for (i, out) in out.as_chunks_mut::<3>().0.iter_mut().enumerate() {
446                        out.copy_from_slice(&evaluate(i, j));
447                    }
448                    Ok(())
449                },
450            )?;
451        }
452        if tangent.iter().any(|&v| !finite(v)) {
453            return Err(Error::NonFinite("plane field tangent overflow".into()));
454        }
455        Ok(tangent)
456    }
457    /// Gradients for a `cotangent` of the field's shape. The pullback recomputes the
458    /// polarization and phase derivatives instead of storing a dense field Jacobian.
459    pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<FieldGradient> {
460        if cotangent.shape() != self.shape() || cotangent.iter().any(|&v| !finite(v)) {
461            return Err(Error::InvalidInput("invalid plane-field cotangent".into()));
462        }
463        let (samples, modes) = (self.points.len(), self.vectors.len());
464        let mut gradient = FieldGradient {
465            coefficients: vec![Complex::default(); self.coefficients.as_ref().map_or(0, Vec::len)],
466            points: vec![[0.0; 3]; samples],
467            vectors: vec![[Complex::default(); 3]; modes],
468        };
469        if samples == 0 {
470            return Ok(gradient);
471        }
472        // The blocks of `PhasesResidual::pullback`; one block adds in the order of a
473        // plain loop over modes and points.
474        let parallel = samples * modes >= PARALLEL_ENTRIES;
475        let block = samples.max(modes).div_ceil(32).max(16);
476        if modes >= samples {
477            // Blocks of modes write the amplitude and wavevector gradients of their
478            // modes and add into partial point gradients. An operator has no amplitude
479            // gradients, so its blocks get none.
480            let mut coefficient_blocks = gradient.coefficients.chunks_mut(block);
481            let blocks: Vec<_> = gradient
482                .vectors
483                .chunks_mut(block)
484                .map(|vectors| (vectors, coefficient_blocks.next()))
485                .collect();
486            gradient.points = try_fold_ordered(
487                blocks,
488                parallel,
489                || vec![[0.0; 3]; samples],
490                |mut points, b, (vectors, mut coefficients)| -> Result<_> {
491                    for (offset, vector) in vectors.iter_mut().enumerate() {
492                        let j = b * block + offset;
493                        let mode = self.mode(j, !self.fixed_vectors)?;
494                        let mut sums = ModeSums::default();
495                        let rows = self.rows(cotangent, j);
496                        self.contract(&mode, &self.points, rows, &mut points, &mut sums);
497                        let coefficient = coefficients.as_deref_mut().map(|c| &mut c[offset]);
498                        self.finish(&mode, &sums, coefficient, vector);
499                    }
500                    Ok(points)
501                },
502                |mut total, partial| {
503                    for (a, b) in total.iter_mut().flatten().zip(partial.iter().flatten()) {
504                        *a += b;
505                    }
506                    total
507                },
508            )?;
509        } else {
510            // Blocks of points write the gradients of their points and add into partial
511            // sums of every mode, which finish once all blocks are added.
512            let terms = (0..modes)
513                .map(|j| self.mode(j, !self.fixed_vectors))
514                .collect::<Result<Vec<_>>>()?;
515            let blocks: Vec<_> = gradient.points.chunks_mut(block).collect();
516            let sums = try_fold_ordered(
517                blocks,
518                parallel,
519                || vec![ModeSums::default(); modes],
520                |mut sums, b, point_gradients| -> Result<_> {
521                    let range = b * block..b * block + point_gradients.len();
522                    let points = &self.points[range.clone()];
523                    for (j, (mode, sums)) in terms.iter().zip(&mut sums).enumerate() {
524                        let rows = &self.rows(cotangent, j)[range.clone()];
525                        self.contract(mode, points, rows, point_gradients, sums);
526                    }
527                    Ok(sums)
528                },
529                |mut total, partial| {
530                    for (total, partial) in total.iter_mut().zip(&partial) {
531                        total.add(partial);
532                    }
533                    total
534                },
535            )?;
536            let mut coefficients = gradient.coefficients.iter_mut();
537            for ((mode, sums), vector) in terms.iter().zip(&sums).zip(&mut gradient.vectors) {
538                self.finish(mode, sums, coefficients.next(), vector);
539            }
540        }
541        Ok(gradient)
542    }
543
544    /// The wavevector, polarization jet and amplitude of plane mode `j`.
545    fn mode(&self, j: usize, variable: bool) -> Result<ModeTerms> {
546        let vector = self.vectors[j];
547        let electric = if variable {
548            polarization_jet::<3>(vector, self.polarizations[j], self.helicity)?
549        } else {
550            polarization(vector, self.polarizations[j], self.helicity)?.map(Jet::<3>::constant)
551        };
552        let coefficient = self
553            .coefficients
554            .as_ref()
555            .map_or(Complex::new(1.0, 0.0), |c| c[j]);
556        Ok(ModeTerms {
557            vector,
558            electric,
559            coefficient,
560        })
561    }
562
563    /// The three Cartesian cotangents of each sample, contiguous in the cotangent
564    /// column of mode `j`, or in the one column of a weighted field.
565    fn rows<'c>(&self, cotangent: &'c DMatrix<Complex>, j: usize) -> &'c [[Complex; 3]] {
566        let length = 3 * self.points.len();
567        let column = if self.coefficients.is_some() { 0 } else { j };
568        cotangent.as_slice()[column * length..][..length]
569            .as_chunks::<3>()
570            .0
571    }
572
573    /// Add the terms of `mode` at `points`, whose cotangents are `rows`, to the point
574    /// gradients `point_gradients` and to the mode's `sums`, in point order.
575    fn contract(
576        &self,
577        mode: &ModeTerms,
578        points: &[[f64; 3]],
579        rows: &[[Complex; 3]],
580        point_gradients: &mut [[f64; 3]],
581        sums: &mut ModeSums,
582    ) {
583        let (weighted, fixed) = (self.coefficients.is_some(), self.fixed_vectors);
584        let ModeTerms {
585            vector,
586            electric,
587            coefficient,
588        } = *mode;
589        // Accumulate through references into the sums, which the loop can keep in
590        // registers, in the same order as indexing them would.
591        let ModeSums {
592            coefficient: coefficient_gradient,
593            vector: vector_gradient,
594            polarization: polarization_cotangent,
595        } = sums;
596        for ((&point, rows), point_gradient) in points.iter().zip(rows).zip(point_gradients) {
597            let phase = phase(vector, point);
598            let weighted_phase = coefficient * phase;
599            let paired: Complex = (0..3).map(|a| rows[a].conj() * electric[a].value).sum();
600            if weighted {
601                *coefficient_gradient += (phase * paired).conj();
602            }
603            let paired_field = weighted_phase * paired * Complex::i();
604            for axis in 0..3 {
605                point_gradient[axis] += (paired_field * vector[axis]).re;
606                if !fixed {
607                    vector_gradient[axis] += (paired_field * point[axis]).conj();
608                    polarization_cotangent[axis] += rows[axis] * weighted_phase.conj();
609                }
610            }
611        }
612    }
613
614    /// Write the amplitude gradient `coefficient` (none for an operator) and the
615    /// wavevector gradient `vector` of `mode` from its sums over all points.
616    fn finish(
617        &self,
618        mode: &ModeTerms,
619        sums: &ModeSums,
620        coefficient: Option<&mut Complex>,
621        vector: &mut [Complex; 3],
622    ) {
623        if let Some(coefficient) = coefficient {
624            *coefficient = sums.coefficient;
625        }
626        *vector = sums.vector;
627        // The polarization is the same at every point: pull it back once per mode.
628        if !self.fixed_vectors {
629            for (axis, gradient) in vector.iter_mut().enumerate() {
630                *gradient += (0..3)
631                    .map(|a| sums.polarization[a] * mode.electric[a].derivative[axis].conj())
632                    .sum::<Complex>();
633            }
634        }
635    }
636}
637
638/// What the field pullback needs of one plane mode at every point.
639#[derive(Clone, Copy)]
640struct ModeTerms {
641    /// The complex wavevector.
642    vector: [Complex; 3],
643    /// The polarization, with its derivatives in the wavevector unless it is fixed.
644    electric: [Jet<3>; 3],
645    /// The amplitude; one for an operator.
646    coefficient: Complex,
647}
648
649/// Sums over points of one plane mode's amplitude, wavevector and polarization
650/// cotangents; the last two stay zero when the wavevectors are fixed.
651#[derive(Clone, Copy, Default)]
652struct ModeSums {
653    coefficient: Complex,
654    vector: [Complex; 3],
655    polarization: [Complex; 3],
656}
657
658impl ModeSums {
659    /// Add the sums over other points.
660    fn add(&mut self, other: &Self) {
661        self.coefficient += other.coefficient;
662        for (a, b) in self
663            .vector
664            .iter_mut()
665            .chain(&mut self.polarization)
666            .zip(other.vector.iter().chain(&other.polarization))
667        {
668            *a += b;
669        }
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::{field, phases};
676    use crate::{
677        Complex, Error,
678        numerics::parallel::PARALLEL_ENTRIES,
679        test_support::{assert_same_bits_on_pools, bits, patterned},
680    };
681
682    /// A wave decaying along +z overflows 400 units below the origin, where its phase
683    /// is `exp(800)`. 350 units below, the phase is finite, but a large sample-point
684    /// tangent overflows the pushforward.
685    #[test]
686    fn overflowing_plane_fields_report_nonfinite() {
687        let k = vec![[0.6.into(), 0.0.into(), Complex::new(0.8, 2.0)]];
688        let at = |z| field(k.clone(), vec![0], vec![[0.0, 0.0, z]], None, true, false);
689        assert!(matches!(at(-400.0), Err(Error::NonFinite(_))));
690        let (_, residual) = at(-350.0).unwrap();
691        let tangent = residual.pushforward(&[], &[[0.0, 0.0, 1e10]], &[[0.0.into(); 3]]);
692        assert!(matches!(tangent, Err(Error::NonFinite(_))));
693    }
694
695    #[test]
696    fn phase_directions_and_gradients_share_one_residual() {
697        let points = vec![[0.2, -0.4, 0.7], [-0.3, 0.1, 0.6]];
698        let vectors = vec![[Complex::new(0.5, 0.1); 3], [Complex::new(-0.3, 0.2); 3]];
699        let (_, residual) = phases(points, vectors).unwrap();
700        let dpoints = [[0.3, 0.2, -0.1], [-0.2, 0.1, 0.4]];
701        let dvectors = [[Complex::new(0.2, -0.1); 3]; 2];
702        let cotangent = patterned(2, 2, 0.8);
703
704        let first = residual.pushforward(&dpoints, &dvectors).unwrap();
705        let gradient = residual.pullback(&cotangent).unwrap();
706        let repeated = residual.pushforward(&dpoints, &dvectors).unwrap();
707        assert_eq!(first, repeated);
708
709        let output_pair: f64 = cotangent
710            .iter()
711            .zip(&first)
712            .map(|(g, d)| (g.conj() * d).re)
713            .sum();
714        let input_pair: f64 = gradient
715            .points
716            .iter()
717            .flatten()
718            .zip(dpoints.iter().flatten())
719            .map(|(g, d)| g * d)
720            .chain(
721                gradient
722                    .vectors
723                    .iter()
724                    .flatten()
725                    .zip(dvectors.iter().flatten())
726                    .map(|(g, d)| (g.conj() * d).re),
727            )
728            .sum();
729        assert!((output_pair - input_pair).abs() < 1e-14);
730    }
731
732    /// Field pullbacks add their partial sums in blocks fixed by the shape: blocks of
733    /// modes and blocks of points, for weighted fields and operators, with and without
734    /// wavevector derivatives, give gradients that repeat bit for bit on every pool
735    /// size once the blocks run in parallel.
736    #[test]
737    fn pullbacks_do_not_depend_on_the_thread_count() {
738        for (samples, modes) in [(30_u32, 200_u32), (300, 20)] {
739            let vectors: Vec<[Complex; 3]> = (0..modes)
740                .map(|j| {
741                    let t = f64::from(j);
742                    let (theta, phi) = (1.55 + 1.3 * (0.61 * t).sin(), 2.3 * t);
743                    [
744                        Complex::new(1.2 * theta.sin() * phi.cos(), 0.0),
745                        Complex::new(1.2 * theta.sin() * phi.sin(), 0.0),
746                        Complex::new(1.2 * theta.cos(), 0.01),
747                    ]
748                })
749                .collect();
750            let polarizations: Vec<u8> = (0..modes).map(|j| u8::from(j % 2 == 0)).collect();
751            let points: Vec<[f64; 3]> = (0..samples)
752                .map(|i| {
753                    let t = f64::from(i);
754                    [(0.37 * t).sin(), (0.53 * t).cos(), 0.8 * (0.71 * t).sin()]
755                })
756                .collect();
757            assert!(points.len() * vectors.len() >= PARALLEL_ENTRIES);
758            for weighted in [true, false] {
759                let coefficients =
760                    weighted.then(|| patterned(vectors.len(), 1, 0.3).as_slice().to_vec());
761                let columns = if weighted { 1 } else { vectors.len() };
762                let g = patterned(3 * points.len(), columns, 0.8);
763                for fixed_vectors in [false, true] {
764                    assert_same_bits_on_pools(|| {
765                        let (_, residual) = field(
766                            vectors.clone(),
767                            polarizations.clone(),
768                            points.clone(),
769                            coefficients.clone(),
770                            true,
771                            fixed_vectors,
772                        )
773                        .unwrap();
774                        let g = residual.pullback(&g).unwrap();
775                        bits(&[&g.coefficients, &g.points, &g.vectors])
776                    });
777                }
778            }
779        }
780    }
781}