1use rayon::prelude::*;
11
12use super::parallel::Parallel;
13use crate::{Complex, Error, Result};
14
15pub(crate) fn size(lengths: &[usize], message: &str) -> Result<usize> {
18 let size = if lengths.contains(&0) {
19 0
20 } else {
21 lengths.iter().copied().max().unwrap_or_default()
22 };
23 if lengths.iter().any(|&n| n != 1 && n != size) {
24 return Err(Error::InvalidInput(message.into()));
25 }
26 Ok(size)
27}
28
29#[allow(clippy::indexing_slicing)] pub(crate) fn element<T: Copy>(values: &[T], i: usize) -> T {
32 values[if values.len() == 1 { 0 } else { i }]
33}
34
35pub(crate) fn map<T: Send>(
37 size: usize,
38 parallel: Parallel,
39 f: impl Fn(usize) -> Result<T> + Sync + Send,
40) -> Result<Vec<T>> {
41 match parallel.chunk(size) {
42 Some(chunk) => crate::threads::install(|| {
43 (0..size)
44 .into_par_iter()
45 .with_min_len(chunk)
46 .map(f)
47 .collect()
48 }),
49 None => (0..size).map(f).collect(),
50 }
51}
52
53pub(crate) fn pushforward<const A: usize, T: Send>(
56 tangents: [&[Complex]; A],
57 size: usize,
58 message: &str,
59 parallel: Parallel,
60 f: impl Fn(usize, [Complex; A]) -> Result<T> + Sync + Send,
61) -> Result<Vec<T>> {
62 if tangents.iter().any(|tangent| {
63 (tangent.len() != 1 && tangent.len() != size)
64 || tangent.iter().any(|&value| !crate::numerics::finite(value))
65 }) {
66 return Err(Error::InvalidInput(message.into()));
67 }
68 map(size, parallel, |i| {
69 f(i, tangents.map(|tangent| element(tangent, i)))
70 })
71}
72
73pub(crate) trait Cotangent: Sync {
75 fn finite(&self) -> bool;
77}
78
79impl Cotangent for Complex {
80 fn finite(&self) -> bool {
81 crate::numerics::finite(*self)
82 }
83}
84
85impl<const N: usize> Cotangent for [Complex; N] {
86 fn finite(&self) -> bool {
87 self.iter().all(|&z| crate::numerics::finite(z))
88 }
89}
90
91pub(crate) fn pullback<G: Cotangent, const A: usize>(
95 cotangent: &[G],
96 size: usize,
97 message: &str,
98 lengths: [usize; A],
99 parallel: Parallel,
100 f: impl Fn(usize, &G) -> Result<[Complex; A]> + Sync + Send,
101) -> Result<[Vec<Complex>; A]> {
102 if cotangent.len() != size || !cotangent.iter().all(Cotangent::finite) {
103 return Err(Error::InvalidInput(message.into()));
104 }
105 let Some(chunk) = parallel.chunk(size) else {
106 let mut sums = [Complex::default(); A];
111 let mut gradients =
112 lengths.map(|length| Vec::with_capacity(if length == 1 { 1 } else { size }));
113 for (i, g) in cotangent.iter().enumerate() {
114 let values = f(i, g)?;
115 for (((gradient, sum), &length), value) in gradients
116 .iter_mut()
117 .zip(&mut sums)
118 .zip(&lengths)
119 .zip(values)
120 {
121 if length == 1 {
122 *sum += value;
123 } else {
124 gradient.push(value);
125 }
126 }
127 }
128 for ((gradient, sum), &length) in gradients.iter_mut().zip(sums).zip(&lengths) {
129 if length == 1 {
130 gradient.push(sum);
131 }
132 }
133 return Ok(gradients);
134 };
135 let mut rows = vec![[Complex::default(); A]; size];
138 crate::threads::install(|| {
139 rows.par_iter_mut()
140 .zip(cotangent)
141 .enumerate()
142 .with_min_len(chunk)
143 .try_for_each(|(i, (row, g))| {
144 *row = f(i, g)?;
145 Ok(())
146 })
147 })?;
148 if A == 1 && lengths != [1; A] {
149 let mut gradient = Some(rows.into_flattened());
151 return Ok(std::array::from_fn(|_| gradient.take().unwrap_or_default()));
152 }
153 Ok(std::array::from_fn(|a| {
155 let gradient = rows
156 .iter()
157 .map(|row| row.get(a).copied().unwrap_or_default());
158 if lengths.get(a) == Some(&1) {
159 vec![gradient.sum()]
160 } else {
161 gradient.collect()
162 }
163 }))
164}
165
166#[cfg(test)]
168mod tests {
169 use proptest::{prelude::*, test_runner::TestCaseError};
170
171 use super::{Parallel, element, map, pullback, pushforward, size};
172 use crate::{Complex, test_support::DEFAULT_CASES};
173
174 #[test]
175 fn size_accepts_scalars_and_one_common_length() {
176 let message = "arrays must have equal lengths or scalar inputs";
177 for (lengths, expected) in [
178 (&[1, 1][..], Some(1)),
179 (&[1, 4, 4], Some(4)),
180 (&[4, 1, 1], Some(4)),
181 (&[0, 1], Some(0)),
182 (&[0], Some(0)),
183 (&[], Some(0)),
184 (&[0, 3], None),
185 (&[2, 3], None),
186 (&[2, 1, 3], None),
187 ] {
188 match size(lengths, message) {
189 Ok(size) => assert_eq!(Some(size), expected, "{lengths:?}"),
190 Err(error) => {
191 assert_eq!(expected, None, "{lengths:?}");
192 assert_eq!(error.to_string(), message);
193 }
194 }
195 }
196 }
197
198 proptest! {
199 #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
200
201 #[test]
202 fn pullback_is_the_transposed_elementwise_product(
203 size in 0_usize..40,
204 scalar in any::<[bool; 2]>(),
205 threshold in 0_usize..48,
206 chunked in any::<bool>(),
207 ) {
208 check_pullback(size, scalar, threshold, chunked)?;
209 }
210 }
211
212 fn check_pullback(
217 size: usize,
218 scalar: [bool; 2],
219 threshold: usize,
220 chunked: bool,
221 ) -> Result<(), TestCaseError> {
222 let parallel = if chunked {
223 Parallel::Chunked(threshold)
224 } else {
225 Parallel::AtLeast(threshold)
226 };
227 let argument = |index: usize, scalar: bool| -> Vec<Complex> {
228 let length = if scalar { 1 } else { size };
229 (0..length)
230 .map(|i| Complex::new(f64::from(u32::try_from(i + index).unwrap()), 0.5))
231 .collect()
232 };
233 let arguments = [argument(1, scalar[0]), argument(3, scalar[1])];
234 let lengths = arguments.each_ref().map(Vec::len);
235 let values = map(size, parallel, |i| {
236 Ok(element(&arguments[0], i) * element(&arguments[1], i))
237 })?;
238 prop_assert_eq!(values.len(), size);
239 for (i, value) in values.iter().enumerate() {
240 prop_assert_eq!(
241 *value,
242 element(&arguments[0], i) * element(&arguments[1], i)
243 );
244 }
245 let cotangent: Vec<_> = (0..size)
246 .map(|i| Complex::new(0.25, f64::from(u32::try_from(i).unwrap())))
247 .collect();
248 let tangents = arguments.each_ref().map(|values| {
249 values
250 .iter()
251 .map(|z| Complex::new(0.3, -0.2) * z)
252 .collect::<Vec<_>>()
253 });
254 let direction = pushforward(
255 tangents.each_ref().map(Vec::as_slice),
256 size,
257 "tangent",
258 parallel,
259 |i, [da, db]| Ok(da * element(&arguments[1], i) + element(&arguments[0], i) * db),
260 )?;
261 for (i, &actual) in direction.iter().enumerate() {
262 let expected = element(&tangents[0], i) * element(&arguments[1], i)
263 + element(&arguments[0], i) * element(&tangents[1], i);
264 prop_assert_eq!(actual, expected);
265 }
266 let gradients = pullback(&cotangent, size, "cotangent", lengths, parallel, |i, &g| {
267 let [a, b] = arguments.each_ref().map(|v| element(v, i));
268 Ok([g * b.conj(), g * a.conj()])
269 })?;
270 for (argument, gradient) in arguments.iter().zip(&gradients) {
271 prop_assert_eq!(gradient.len(), argument.len());
272 }
273 for (index, gradient) in gradients.iter().enumerate() {
274 let other = &arguments[1 - index];
275 let expected: Vec<_> = (0..size)
276 .map(|i| cotangent[i] * element(other, i).conj())
277 .collect();
278 let expected = if lengths[index] == 1 {
279 vec![expected.iter().sum()]
280 } else {
281 expected
282 };
283 prop_assert_eq!(gradient, &expected);
285 }
286 let [single] = pullback(
288 &cotangent,
289 size,
290 "cotangent",
291 [lengths[0]],
292 parallel,
293 |i, &g| Ok([g * element(&arguments[1], i).conj()]),
294 )?;
295 prop_assert_eq!(&single, &gradients[0]);
296 Ok(())
297 }
298
299 #[test]
300 fn pullback_rejects_mismatched_or_nonfinite_cotangents() {
301 let zero = |_: usize, _: &[Complex; 2]| Ok([Complex::default(); 1]);
302 let finite = [[Complex::new(0.5, 0.0); 2]; 2];
303 let parallel = Parallel::AtLeast(1024);
304 assert!(pullback(&finite, 2, "cotangent", [2], parallel, zero).is_ok());
305 assert!(pullback(&finite, 3, "cotangent", [3], parallel, zero).is_err());
306 assert!(pullback(&finite[..1], 2, "cotangent", [2], parallel, zero).is_err());
307 let mut nonfinite = finite;
308 nonfinite[1][0].im = f64::NAN;
309 let error = pullback(&nonfinite, 2, "cotangent", [2], parallel, zero).unwrap_err();
310 assert_eq!(error.to_string(), "cotangent");
311 }
312
313 #[test]
314 fn pushforward_rejects_mismatched_or_nonfinite_tangents() {
315 let parallel = Parallel::AtLeast(1024);
316 let identity = |_: usize, [value]: [Complex; 1]| Ok(value);
317 let finite = [Complex::new(0.5, 0.2); 2];
318 assert!(pushforward([&finite], 2, "tangent", parallel, identity).is_ok());
319 assert!(pushforward([&finite[..1]], 2, "tangent", parallel, identity).is_ok());
320 assert!(pushforward([&finite], 3, "tangent", parallel, identity).is_err());
321 assert!(pushforward([&[]], 2, "tangent", parallel, identity).is_err());
322 assert!(pushforward([&[]], 0, "tangent", parallel, identity).is_ok());
323 let bad = [Complex::new(f64::NAN, 0.0)];
324 assert!(pushforward([&bad], 2, "tangent", parallel, identity).is_err());
325 }
326}