1use super::{
7 SumGradient, SumPart, SumTangent, cell::BlochLattice, derivatives_part, sum_part, wave::Family,
8};
9use crate::{
10 Complex, Error, Result,
11 numerics::{broadcast, finite, parallel::Parallel},
12};
13
14const PARALLEL: Parallel = Parallel::AtLeast(8);
16
17pub fn sum_array(
25 waves: Vec<Family>,
26 wavenumbers: Vec<Complex>,
27 lattices: Vec<BlochLattice>,
28 shifts: Vec<[f64; 3]>,
29 etas: Vec<Complex>,
30 parts: Vec<SumPart>,
31) -> Result<(Vec<Complex>, SumResidual)> {
32 let residual = SumResidual::new(waves, wavenumbers, lattices, shifts, etas, parts)?;
33 Ok((residual.values()?, residual))
34}
35
36#[derive(Debug)]
39pub struct SumResidual {
40 waves: Vec<Family>,
41 wavenumbers: Vec<Complex>,
42 lattices: Vec<BlochLattice>,
43 shifts: Vec<[f64; 3]>,
44 etas: Vec<Complex>,
45 parts: Vec<SumPart>,
46 size: usize,
47}
48
49impl SumResidual {
50 pub fn new(
52 waves: Vec<Family>,
53 wavenumbers: Vec<Complex>,
54 lattices: Vec<BlochLattice>,
55 shifts: Vec<[f64; 3]>,
56 etas: Vec<Complex>,
57 parts: Vec<SumPart>,
58 ) -> Result<Self> {
59 let size = broadcast::size(
60 &[
61 waves.len(),
62 wavenumbers.len(),
63 lattices.len(),
64 shifts.len(),
65 etas.len(),
66 parts.len(),
67 ],
68 "arrays must have equal lengths or scalar inputs",
69 )?;
70 if parts
71 .iter()
72 .any(|p| matches!(p, SumPart::Real | SumPart::Reciprocal))
73 && etas.contains(&Complex::default())
74 {
75 return Err(Error::InvalidInput(
76 "component adjoints require an explicit nonzero Ewald split".into(),
77 ));
78 }
79 Ok(Self {
80 waves,
81 wavenumbers,
82 lattices,
83 shifts,
84 etas,
85 parts,
86 size,
87 })
88 }
89
90 pub fn values(&self) -> Result<Vec<Complex>> {
92 broadcast::map(self.size, PARALLEL, |i| {
93 let (wave, k, lattice, r, eta, part) = self.element(i);
94 sum_part(wave, k, lattice, r, eta, part)
95 })
96 }
97 pub fn pushforward(&self, tangents: &[SumTangent]) -> Result<Vec<Complex>> {
100 if (tangents.len() != 1 && tangents.len() != self.size)
101 || tangents.iter().any(|t| !t.is_finite())
102 {
103 return Err(Error::InvalidInput(
104 "tangents must be finite and match lattice output".into(),
105 ));
106 }
107 broadcast::map(self.size, PARALLEL, |i| {
108 let tangent = broadcast::element(tangents, i);
109 let (wave, k, lattice, r, eta, part) = self.element(i);
110 Ok(derivatives_part(wave, k, lattice, r, eta, part)?.pushforward(&tangent))
111 })
112 }
113
114 fn element(&self, i: usize) -> (Family, Complex, &BlochLattice, [f64; 3], Complex, SumPart) {
116 (
117 broadcast::element(&self.waves, i),
118 broadcast::element(&self.wavenumbers, i),
119 &self.lattices[if self.lattices.len() == 1 { 0 } else { i }],
120 broadcast::element(&self.shifts, i),
121 broadcast::element(&self.etas, i),
122 broadcast::element(&self.parts, i),
123 )
124 }
125
126 pub fn pullback(&self, cotangent: &[Complex]) -> Result<Vec<SumGradient>> {
129 if cotangent.len() != self.size || cotangent.iter().any(|&g| !finite(g)) {
130 return Err(Error::InvalidInput(
131 "cotangent must be finite and match lattice output".into(),
132 ));
133 }
134 broadcast::map(self.size, PARALLEL, |i| {
135 let g = cotangent[i];
136 if g == Complex::default() {
137 return Ok(SumGradient::default());
138 }
139 let (wave, k, lattice, r, eta, part) = self.element(i);
140 Ok(derivatives_part(wave, k, lattice, r, eta, part)?.pullback(g))
141 })
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn residual_reuses_inputs_across_forward_and_reverse_directions() {
151 let (_, residual) = sum_array(
152 vec![Family::Spherical { l: 2, m: -1 }],
153 vec![Complex::new(2.1, 0.2)],
154 vec![BlochLattice::new(&[vec![1.6]], &[0.1]).unwrap()],
155 vec![[0.19, 0.11, 0.07], [0.21, -0.09, 0.05]],
156 vec![Complex::new(0.9, 0.03)],
157 vec![SumPart::Full],
158 )
159 .unwrap();
160 let direction = SumTangent {
161 k: Complex::new(0.1, -0.2),
162 ..SumTangent::default()
163 };
164 let first = residual.pushforward(&[direction]).unwrap();
165 let cotangent = [Complex::new(0.3, 0.4), Complex::new(-0.2, 0.1)];
166 let gradient = residual.pullback(&cotangent).unwrap();
167 let opposite = residual
168 .pushforward(&[SumTangent {
169 k: -direction.k,
170 ..SumTangent::default()
171 }])
172 .unwrap();
173 for ((forward, reverse), (g, negated)) in first
174 .iter()
175 .zip(&gradient)
176 .zip(cotangent.iter().zip(opposite))
177 {
178 let pairing = (g.conj() * forward).re;
179 let transposed = (reverse.k.conj() * direction.k).re;
180 assert!((pairing - transposed).abs() < 1e-12 * (1.0 + pairing.abs()));
181 assert_eq!(*forward, -negated);
182 }
183 assert_eq!(first, residual.pushforward(&[direction]).unwrap());
184 }
185}