1#![allow(clippy::indexing_slicing)] use std::collections::HashMap;
16
17use nalgebra::DMatrix;
18use rayon::prelude::*;
19
20use crate::{
21 Complex, Error, Result,
22 basis::{ModeLabel, MultipoleBasis},
23 fields::{SPATIAL_AND_K, VALUES, VectorWave, WaveSet},
24 numerics::{self, finite},
25 saved::{Reader, SavedState, Writer, invalid},
26 special::Radial,
27 sw::Mode,
28};
29
30#[derive(Debug)]
33pub struct Surface {
34 pub theta: Vec<f64>,
36 pub weights: Vec<f64>,
38 pub radii: Vec<f64>,
40 pub slopes: Vec<f64>,
43}
44
45#[derive(Debug)]
48pub struct QmatResidual {
49 destination: Vec<Mode>,
50 source: Vec<Mode>,
51 surface: Surface,
52 ks: [[Complex; 2]; 2],
53 zs: [Complex; 2],
54 radial: Radial,
55 radial_area_factor: bool,
56}
57
58#[derive(Debug)]
60pub struct QmatGradient {
61 pub radii: Vec<f64>,
63 pub slopes: Vec<f64>,
65 pub ks: [[Complex; 2]; 2],
68 pub zs: [Complex; 2],
70}
71
72fn node_geometry(surface: &Surface, i: usize) -> [[f64; 3]; 4] {
76 let (s, c) = surface.theta[i].sin_cos();
77 let rhat = [s, 0.0, c];
78 let slope = [-c, 0.0, s];
79 [
80 rhat.map(|x| x * surface.radii[i]),
81 std::array::from_fn(|j| surface.radii[i] * rhat[j] + surface.slopes[i] * slope[j]),
82 rhat,
83 slope,
84 ]
85}
86
87fn cross(a: [Complex; 3], n: [f64; 3]) -> [Complex; 3] {
89 [
90 a[1] * n[2] - a[2] * n[1],
91 a[2] * n[0] - a[0] * n[2],
92 a[0] * n[1] - a[1] * n[0],
93 ]
94}
95
96fn dot(a: [Complex; 3], b: [Complex; 3]) -> Complex {
98 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
99}
100
101fn triple(n: [f64; 3], a: [Complex; 3], b: [Complex; 3]) -> Complex {
103 n[0] * (a[1] * b[2] - a[2] * b[1])
104 + n[1] * (a[2] * b[0] - a[0] * b[2])
105 + n[2] * (a[0] * b[1] - a[1] * b[0])
106}
107
108fn origin_waves(
112 modes: impl Iterator<Item = Mode>,
113 ks: [Complex; 2],
114 radial: Radial,
115) -> Result<WaveSet> {
116 let modes: Vec<_> = modes.map(|mode| (0, mode)).collect();
117 let used = |pol: usize| modes.iter().any(|(_, mode)| usize::from(mode.pol) == pol);
118 let ks = [0, 1].map(|pol| if used(pol) { ks[pol] } else { ks[1 - pol] });
119 let basis = crate::sw::Basis {
120 modes,
121 positions: vec![[0.0; 3]],
122 };
123 WaveSet::new(MultipoleBasis::Spherical(basis), ks, true, radial)
124}
125
126fn evaluate_waves<const N: usize>(
130 waves: &WaveSet,
131 count: usize,
132 point: [f64; 3],
133) -> Result<Vec<VectorWave>> {
134 let mut cache = waves.cache::<N>();
135 let mut values = Vec::with_capacity(count);
138 for i in 0..count {
139 values.push(waves.wave::<N>(i, point, &mut cache)?.0);
140 }
141 Ok(values)
142}
143
144pub fn qmat(
155 destination: Vec<Mode>,
156 source: Vec<Mode>,
157 surface: Surface,
158 ks: [[Complex; 2]; 2],
159 zs: [Complex; 2],
160 radial: Radial,
161 radial_area_factor: bool,
162) -> Result<(DMatrix<Complex>, QmatResidual)> {
163 let n = surface.theta.len();
164 let residual = QmatResidual {
165 destination,
166 source,
167 surface,
168 ks,
169 zs,
170 radial,
171 radial_area_factor,
172 };
173 residual.validate()?;
174 let [destination, source] = residual.wave_sets()?;
176 let samples = crate::threads::install(|| {
177 (0..n)
178 .into_par_iter()
179 .map(|node| -> Result<Sample> {
180 let [point, normal, _, _] = node_geometry(&residual.surface, node);
181 let values = |waves: Vec<VectorWave>| waves.into_iter().map(|w| w.value).collect();
182 Ok(Sample {
183 weight: residual.weight(node),
184 normal,
185 destination: values(evaluate_waves::<VALUES>(
186 &destination,
187 residual.destination.len(),
188 point,
189 )?),
190 source: values(evaluate_waves::<VALUES>(
191 &source,
192 residual.source.len(),
193 point,
194 )?),
195 })
196 })
197 .collect::<Result<Vec<_>>>()
198 })?;
199 let rows = residual.rows_by_order();
200 let factors = residual.factors();
201 let mut value = numerics::zeros(residual.destination.len(), residual.source.len())?;
202 crate::threads::install(|| {
204 value
205 .as_mut_slice()
206 .par_chunks_mut(residual.destination.len())
207 .zip(&residual.source)
208 .enumerate()
209 .for_each(|(j, (column, from))| {
210 let rows = rows.get(&from.m).map_or(&[][..], Vec::as_slice);
211 for sample in &samples {
212 for &i in rows {
213 let factor = factors[usize::from(residual.destination[i].pol)]
214 [usize::from(from.pol)];
215 column[i] += sample.weight
216 * factor
217 * triple(sample.normal, sample.destination[i], sample.source[j]);
218 }
219 }
220 });
221 });
222 if value.iter().any(|&z| !finite(z)) {
223 return Err(Error::NonFinite("non-finite EBCM integral".into()));
224 }
225 Ok((value, residual))
226}
227
228const HELICITY: [f64; 2] = [-1.0, 1.0];
233
234struct Sample {
236 weight: f64,
237 normal: [f64; 3],
238 destination: Vec<[Complex; 3]>,
239 source: Vec<[Complex; 3]>,
240}
241
242struct SampleTangent {
245 value: Sample,
246 weight: f64,
247 normal: [f64; 3],
248 destination: Vec<[Complex; 3]>,
249 source: Vec<[Complex; 3]>,
250}
251
252impl QmatResidual {
253 fn validate(&self) -> Result<()> {
254 let surface = &self.surface;
255 let n = surface.theta.len();
256 if n == 0
257 || self.destination.is_empty()
258 || self.source.is_empty()
259 || surface.weights.len() != n
260 || surface.radii.len() != n
261 || surface.slopes.len() != n
262 || surface
263 .theta
264 .iter()
265 .any(|t| !t.is_finite() || !(0.0..=std::f64::consts::PI).contains(t))
266 || surface.radii.iter().any(|r| !r.is_finite() || *r <= 0.0)
267 || surface
268 .weights
269 .iter()
270 .chain(&surface.slopes)
271 .any(|x| !x.is_finite())
272 || self
273 .ks
274 .iter()
275 .flatten()
276 .chain(&self.zs)
277 .any(|&z| !finite(z))
278 {
279 return Err(Error::InvalidInput("require nonempty modes, finite matching surface samples, positive radii and polar angles in [0, pi]".into()));
280 }
281 for mode in self.destination.iter().chain(&self.source) {
282 mode.validate()?;
283 }
284 Ok(())
285 }
286
287 pub fn state_size(samples: usize, destinations: usize, sources: usize) -> Result<usize> {
289 if samples == 0 || destinations == 0 || sources == 0 {
290 return Err(invalid());
291 }
292 destinations
295 .checked_add(sources)
296 .and_then(|modes| modes.checked_mul(9))
297 .and_then(|modes| samples.checked_mul(32).and_then(|n| modes.checked_add(n)))
298 .and_then(|bytes| bytes.checked_add(122))
299 .ok_or_else(invalid)
300 }
301
302 fn wave_sets(&self) -> Result<[WaveSet; 2]> {
304 let reversed = self
305 .destination
306 .iter()
307 .map(|&mode| Mode { m: -mode.m, ..mode });
308 Ok([
309 origin_waves(reversed, self.ks[0], Radial::Regular)?,
310 origin_waves(self.source.iter().copied(), self.ks[1], self.radial)?,
311 ])
312 }
313
314 fn weight(&self, node: usize) -> f64 {
317 self.measure(node)
318 * if self.radial_area_factor {
319 self.surface.radii[node]
320 } else {
321 1.0
322 }
323 }
324
325 fn measure(&self, node: usize) -> f64 {
327 self.surface.weights[node] * self.surface.theta[node].sin()
328 }
329
330 fn rows_by_order(&self) -> HashMap<i32, Vec<usize>> {
332 let mut rows = HashMap::<i32, Vec<usize>>::new();
333 for (i, mode) in self.destination.iter().enumerate() {
334 rows.entry(mode.m).or_default().push(i);
335 }
336 rows
337 }
338
339 fn factors(&self) -> [[Complex; 2]; 2] {
342 HELICITY.map(|a| HELICITY.map(|b| a * self.zs[1] + b * self.zs[0]))
343 }
344
345 #[must_use]
347 pub fn shape(&self) -> (usize, usize) {
348 (self.destination.len(), self.source.len())
349 }
350
351 #[must_use]
353 pub fn samples(&self) -> usize {
354 self.surface.radii.len()
355 }
356
357 pub fn pushforward(
362 &self,
363 radii: &[f64],
364 slopes: &[f64],
365 ks: [[Complex; 2]; 2],
366 zs: [Complex; 2],
367 ) -> Result<DMatrix<Complex>> {
368 if radii.len() != self.samples()
369 || slopes.len() != self.samples()
370 || radii.iter().chain(slopes).any(|x| !x.is_finite())
371 || ks.iter().flatten().chain(&zs).any(|&z| !finite(z))
372 {
373 return Err(Error::InvalidInput("invalid EBCM tangents".into()));
374 }
375 let [destination, source] = self.wave_sets()?;
376 let samples = crate::threads::install(|| {
377 (0..self.samples())
378 .into_par_iter()
379 .map(|node| -> Result<SampleTangent> {
380 let [point, normal, rhat, slope] = node_geometry(&self.surface, node);
381 let point_tangent = rhat.map(|x| x * radii[node]);
382 let contract = |waves: Vec<VectorWave>, modes: &[Mode], dk: [Complex; 2]| {
383 waves
384 .into_iter()
385 .zip(modes)
386 .map(|(wave, mode)| {
387 let tangent = std::array::from_fn(|component| {
388 wave.k[component] * dk[usize::from(mode.pol)]
389 + (0..3)
390 .map(|axis| {
391 wave.position[component][axis] * point_tangent[axis]
392 })
393 .sum::<Complex>()
394 });
395 (wave.value, tangent)
396 })
397 .unzip()
398 };
399 let (a, da) = contract(
400 evaluate_waves::<SPATIAL_AND_K>(
401 &destination,
402 self.destination.len(),
403 point,
404 )?,
405 &self.destination,
406 ks[0],
407 );
408 let (b, db) = contract(
409 evaluate_waves::<SPATIAL_AND_K>(&source, self.source.len(), point)?,
410 &self.source,
411 ks[1],
412 );
413 Ok(SampleTangent {
414 value: Sample {
415 weight: self.weight(node),
416 normal,
417 destination: a,
418 source: b,
419 },
420 weight: if self.radial_area_factor {
421 self.measure(node) * radii[node]
422 } else {
423 0.0
424 },
425 normal: std::array::from_fn(|axis| {
426 radii[node] * rhat[axis] + slopes[node] * slope[axis]
427 }),
428 destination: da,
429 source: db,
430 })
431 })
432 .collect::<Result<Vec<_>>>()
433 })?;
434 let rows = self.rows_by_order();
435 let factors = self.factors();
436 let factor_tangents = HELICITY.map(|a| HELICITY.map(|b| a * zs[1] + b * zs[0]));
437 let mut tangent = numerics::zeros(self.destination.len(), self.source.len())?;
438 crate::threads::install(|| {
439 tangent
440 .as_mut_slice()
441 .par_chunks_mut(self.destination.len())
442 .zip(&self.source)
443 .enumerate()
444 .for_each(|(j, (column, from))| {
445 let rows = rows.get(&from.m).map_or(&[][..], Vec::as_slice);
446 let pb = usize::from(from.pol);
447 for sample in &samples {
448 let value = &sample.value;
449 for &i in rows {
450 let pa = usize::from(self.destination[i].pol);
451 let factor = factors[pa][pb];
452 let a = value.destination[i];
453 let b = value.source[j];
454 let integral = triple(value.normal, a, b);
455 let derivative = triple(sample.normal, a, b)
456 + triple(value.normal, sample.destination[i], b)
457 + triple(value.normal, a, sample.source[j]);
458 column[i] += (sample.weight * factor
459 + value.weight * factor_tangents[pa][pb])
460 * integral
461 + value.weight * factor * derivative;
462 }
463 }
464 });
465 });
466 if tangent.iter().any(|&z| !finite(z)) {
467 return Err(Error::NonFinite("non-finite EBCM tangent".into()));
468 }
469 Ok(tangent)
470 }
471
472 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<QmatGradient> {
477 if cotangent.shape() != self.shape() || cotangent.iter().any(|&z| !finite(z)) {
478 return Err(Error::InvalidInput("invalid EBCM cotangent".into()));
479 }
480 let rows = self.rows_by_order();
481 let factors = self.factors();
482 let [destination, source] = self.wave_sets()?;
483 let nodes = crate::threads::install(|| {
484 (0..self.surface.theta.len())
485 .into_par_iter()
486 .map(|node| {
487 let [point, normal, rhat, slope] = node_geometry(&self.surface, node);
488 let a = evaluate_waves::<SPATIAL_AND_K>(
489 &destination,
490 self.destination.len(),
491 point,
492 )?;
493 let b = evaluate_waves::<SPATIAL_AND_K>(&source, self.source.len(), point)?;
494 let (measure, weight) = (self.measure(node), self.weight(node));
495 let along_radius = |wave: &VectorWave| -> [Complex; 3] {
496 std::array::from_fn(|c| (0..3).map(|d| wave.position[c][d] * rhat[d]).sum())
497 };
498 let ar: Vec<_> = a.iter().map(along_radius).collect();
499 let mut result = NodeGradient::default();
500 for (j, (vb, from)) in b.iter().zip(&self.source).enumerate() {
501 let pb = usize::from(from.pol);
502 let normal_cross = cross(vb.value, normal);
505 let rhat_cross = cross(vb.value, rhat);
506 let slope_cross = cross(vb.value, slope);
507 let radius_cross = cross(along_radius(vb), normal);
508 let k_cross = cross(vb.k, normal);
509 for &i in rows.get(&from.m).into_iter().flatten() {
510 let cot = cotangent[(i, j)];
511 if cot == Complex::default() {
512 continue;
513 }
514 let va = &a[i];
515 let pa = usize::from(self.destination[i].pol);
516 let factor = factors[pa][pb];
517 let t = dot(va.value, normal_cross);
518 let dr = dot(va.value, rhat_cross)
519 + dot(ar[i], normal_cross)
520 + dot(va.value, radius_cross);
521 let scaled = cot.conj() * weight;
522 let weighted = scaled * factor;
523 result.radius += (weighted * dr).re;
524 if self.radial_area_factor {
525 result.radius += (cot.conj() * measure * factor * t).re;
526 }
527 result.slope += (weighted * dot(va.value, slope_cross)).re;
528 result.ks[0][pa] += (weighted * dot(va.k, normal_cross)).conj();
529 result.ks[1][pb] += (weighted * dot(va.value, k_cross)).conj();
530 result.zs[0] += (scaled * HELICITY[pb] * t).conj();
531 result.zs[1] += (scaled * HELICITY[pa] * t).conj();
532 }
533 }
534 Ok(result)
535 })
536 .collect::<Result<Vec<_>>>()
537 })?;
538 let mut result = QmatGradient {
539 radii: Vec::with_capacity(nodes.len()),
540 slopes: Vec::with_capacity(nodes.len()),
541 ks: [[Complex::default(); 2]; 2],
542 zs: [Complex::default(); 2],
543 };
544 for node in nodes {
545 result.radii.push(node.radius);
546 result.slopes.push(node.slope);
547 for side in 0..2 {
548 result.zs[side] += node.zs[side];
549 for pol in 0..2 {
550 result.ks[side][pol] += node.ks[side][pol];
551 }
552 }
553 }
554 Ok(result)
555 }
556}
557
558impl SavedState for QmatResidual {
559 fn save_state(&self) -> Result<Vec<u8>> {
560 let mut writer = Writer::new(Self::state_size(
561 self.samples(),
562 self.destination.len(),
563 self.source.len(),
564 )?);
565 writer.usize(self.samples());
566 writer.usize(self.destination.len());
567 writer.usize(self.source.len());
568 writer.byte(u8::from(self.radial == Radial::Singular));
569 writer.byte(u8::from(self.radial_area_factor));
570 for mode in self.destination.iter().chain(&self.source) {
571 writer.i32(mode.l);
572 writer.i32(mode.m);
573 writer.byte(mode.pol);
574 }
575 for values in [
576 &self.surface.theta,
577 &self.surface.weights,
578 &self.surface.radii,
579 &self.surface.slopes,
580 ] {
581 for &value in values {
582 writer.f64(value);
583 }
584 }
585 for &value in self.ks.iter().flatten().chain(&self.zs) {
586 writer.complex(value);
587 }
588 Ok(writer.finish())
589 }
590
591 fn from_state(bytes: &[u8]) -> Result<Self> {
592 let mut reader = Reader::new(bytes);
593 let samples = reader.usize()?;
594 let destinations = reader.usize()?;
595 let sources = reader.usize()?;
596 if bytes.len() != Self::state_size(samples, destinations, sources)? {
597 return Err(invalid());
598 }
599 let radial = match reader.byte()? {
600 0 => Radial::Regular,
601 1 => Radial::Singular,
602 _ => return Err(invalid()),
603 };
604 let radial_area_factor = match reader.byte()? {
605 0 => false,
606 1 => true,
607 _ => return Err(invalid()),
608 };
609 let mut modes = |count| {
610 (0..count)
611 .map(|_| {
612 Ok(Mode {
613 l: reader.i32()?,
614 m: reader.i32()?,
615 pol: reader.byte()?,
616 })
617 })
618 .collect::<Result<Vec<_>>>()
619 };
620 let destination = modes(destinations)?;
621 let source = modes(sources)?;
622 let mut surface_values = || {
623 (0..samples)
624 .map(|_| reader.f64())
625 .collect::<Result<Vec<_>>>()
626 };
627 let surface = Surface {
628 theta: surface_values()?,
629 weights: surface_values()?,
630 radii: surface_values()?,
631 slopes: surface_values()?,
632 };
633 let ks = [
634 [reader.complex()?, reader.complex()?],
635 [reader.complex()?, reader.complex()?],
636 ];
637 let zs = [reader.complex()?, reader.complex()?];
638 reader.finish()?;
639 let residual = Self {
640 destination,
641 source,
642 surface,
643 ks,
644 zs,
645 radial,
646 radial_area_factor,
647 };
648 residual.validate()?;
649 Ok(residual)
650 }
651}
652
653#[derive(Default)]
656struct NodeGradient {
657 radius: f64,
658 slope: f64,
659 ks: [[Complex; 2]; 2],
660 zs: [Complex; 2],
661}
662
663#[cfg(test)]
664mod tests {
665 #![allow(clippy::float_cmp)] use super::{QmatResidual, Surface, qmat};
668 use crate::{Complex, Result, saved::SavedState, special::Radial, sw::Mode};
669 use nalgebra::DMatrix;
670
671 #[test]
674 fn unused_helicities_accept_zero_wavenumbers() {
675 let surface = || Surface {
676 theta: vec![0.5, 1.5, 2.5],
677 weights: vec![1.0; 3],
678 radii: vec![0.3, 0.35, 0.3],
679 slopes: vec![0.1, 0.0, -0.1],
680 };
681 let modes = vec![Mode { l: 1, m: 0, pol: 1 }, Mode { l: 2, m: 1, pol: 1 }];
682 let (k, zero) = (Complex::new(1.3, 0.1), Complex::default());
683 let zs = [Complex::new(1.0, 0.0), Complex::new(0.8, 0.1)];
684 let q = |ks| {
685 qmat(
686 modes.clone(),
687 modes.clone(),
688 surface(),
689 ks,
690 zs,
691 Radial::Singular,
692 true,
693 )
694 };
695 let (value, residual) = q([[zero, k], [zero, k * 2.0]]).unwrap();
696 let (expected, _) = q([[k, k], [k, k * 2.0]]).unwrap();
697 assert_eq!(value, expected);
698 let gradient = residual.pullback(&DMatrix::from_element(2, 2, k)).unwrap();
699 assert_eq!(gradient.ks[0][0], zero);
700 assert!(q([[k, zero], [k, k]]).is_err());
701 }
702
703 fn state_fixture(radial: Radial, radial_area_factor: bool) -> Result<QmatResidual> {
704 let destination = vec![
705 Mode { l: 1, m: 0, pol: 0 },
706 Mode { l: 2, m: 1, pol: 1 },
707 Mode {
708 l: 1,
709 m: -1,
710 pol: 0,
711 },
712 ];
713 let source = vec![Mode { l: 2, m: 1, pol: 0 }, Mode { l: 1, m: 0, pol: 1 }];
714 let surface = Surface {
715 theta: vec![0.5, 1.5, 2.5],
716 weights: vec![0.7, 0.9, 0.8],
717 radii: vec![0.3, 0.35, 0.3],
718 slopes: vec![0.1, 0.0, -0.1],
719 };
720 let ks = [
721 [Complex::new(1.3, 0.1), Complex::new(1.4, 0.12)],
722 [Complex::new(1.1, 0.0), Complex::new(1.2, 0.02)],
723 ];
724 let zs = [Complex::new(0.9, 0.03), Complex::new(0.8, 0.1)];
725 qmat(
726 destination,
727 source,
728 surface,
729 ks,
730 zs,
731 radial,
732 radial_area_factor,
733 )
734 .map(|(_, residual)| residual)
735 }
736
737 #[test]
738 fn saved_state_preserves_repeated_derivatives() -> Result<()> {
739 let radii = [0.03, -0.02, 0.01];
740 let slopes = [0.02, 0.01, -0.04];
741 let ks = [
742 [Complex::new(0.02, 0.01), Complex::new(-0.03, 0.02)],
743 [Complex::new(0.01, -0.02), Complex::new(-0.02, -0.01)],
744 ];
745 let zs = [Complex::new(0.02, -0.01), Complex::new(-0.01, 0.03)];
746 let cotangent = DMatrix::from_row_slice(
747 3,
748 2,
749 &[
750 Complex::new(0.1, 0.2),
751 Complex::new(-0.2, 0.3),
752 Complex::new(0.3, -0.1),
753 Complex::new(-0.4, 0.1),
754 Complex::new(0.2, 0.4),
755 Complex::new(0.1, -0.3),
756 ],
757 );
758 for radial in [Radial::Regular, Radial::Singular] {
759 for radial_area_factor in [false, true] {
760 let residual = state_fixture(radial, radial_area_factor)?;
761 let state = residual.save_state()?;
762 assert_eq!(state.len(), QmatResidual::state_size(3, 3, 2)?);
763 let restored = QmatResidual::from_state(&state)?;
764 assert_eq!(restored.shape(), (3, 2));
765 assert_eq!(restored.samples(), 3);
766 let expected = residual.pushforward(&radii, &slopes, ks, zs)?;
767 let expected_gradient = residual.pullback(&cotangent)?;
768 for _ in 0..2 {
769 assert_eq!(restored.pushforward(&radii, &slopes, ks, zs)?, expected);
770 let gradient = restored.pullback(&cotangent)?;
771 assert_eq!(gradient.radii, expected_gradient.radii);
772 assert_eq!(gradient.slopes, expected_gradient.slopes);
773 assert_eq!(gradient.ks, expected_gradient.ks);
774 assert_eq!(gradient.zs, expected_gradient.zs);
775 }
776 assert_eq!(restored.save_state()?, state);
777 }
778 }
779 Ok(())
780 }
781
782 #[test]
783 fn saved_state_rejects_malformed_layout_and_invalid_domain() -> Result<()> {
784 let state = state_fixture(Radial::Singular, true)?.save_state()?;
785 for length in [0, 1, 23, 24, 25, state.len() - 1] {
786 assert!(QmatResidual::from_state(&state[..length]).is_err());
787 }
788 let mut trailing = state.clone();
789 trailing.push(0);
790 assert!(QmatResidual::from_state(&trailing).is_err());
791 for offset in [0, 8, 16] {
792 for count in [0_u64, u64::MAX] {
793 let mut malformed = state.clone();
794 malformed[offset..offset + 8].copy_from_slice(&count.to_le_bytes());
795 assert!(QmatResidual::from_state(&malformed).is_err());
796 }
797 }
798 for offset in [24, 25, 34] {
800 let mut malformed = state.clone();
801 malformed[offset] = 2;
802 assert!(QmatResidual::from_state(&malformed).is_err());
803 }
804 let mut malformed = state;
805 let first_radius = 26 + 9 * 5 + 16 * 3;
806 malformed[first_radius..first_radius + 8].copy_from_slice(&f64::NAN.to_le_bytes());
807 assert!(QmatResidual::from_state(&malformed).is_err());
808 assert!(QmatResidual::state_size(usize::MAX, 1, 1).is_err());
809 assert!(QmatResidual::state_size(1, usize::MAX, 1).is_err());
810 assert!(QmatResidual::state_size(1, 1, usize::MAX).is_err());
811 Ok(())
812 }
813}