1#![allow(clippy::indexing_slicing)] use 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#[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#[derive(Debug)]
31pub struct PhasesResidual {
32 pub(super) points: Vec<[f64; 3]>,
33 pub(super) vectors: Vec<[Complex; 3]>,
34}
35#[derive(Debug)]
37pub struct PhasesGradient {
38 pub points: Vec<[f64; 3]>,
40 pub vectors: Vec<[Complex; 3]>,
42}
43
44#[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
61pub 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 #[must_use]
97 pub fn shape(&self) -> (usize, usize) {
98 (self.points.len(), self.vectors.len())
99 }
100
101 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 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 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 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#[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#[derive(Debug)]
252pub struct FieldGradient {
253 pub coefficients: Vec<Complex>,
255 pub points: Vec<[f64; 3]>,
257 pub vectors: Vec<[Complex; 3]>,
259}
260pub 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 #[must_use]
346 pub fn coefficient_count(&self) -> usize {
347 self.coefficients.as_ref().map_or(0, Vec::len)
348 }
349
350 #[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 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 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 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 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 let parallel = samples * modes >= PARALLEL_ENTRIES;
475 let block = samples.max(modes).div_ceil(32).max(16);
476 if modes >= samples {
477 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 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 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 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 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 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 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 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#[derive(Clone, Copy)]
640struct ModeTerms {
641 vector: [Complex; 3],
643 electric: [Jet<3>; 3],
645 coefficient: Complex,
647}
648
649#[derive(Clone, Copy, Default)]
652struct ModeSums {
653 coefficient: Complex,
654 vector: [Complex; 3],
655 polarization: [Complex; 3],
656}
657
658impl ModeSums {
659 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 #[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 #[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}