1#![allow(clippy::indexing_slicing)] mod gmres;
20mod saved;
21
22use std::sync::OnceLock;
23
24pub use gmres::{Convergence, GmresOptions};
25pub(crate) use gmres::{gmres, gmres_batch, norm};
26
27use faer::{
28 Accum, Conj, Mat, MatMut, MatRef, Spec,
29 diag::Diag,
30 dyn_stack::{MemBuffer, MemStack, StackReq},
31 linalg::{
32 evd,
33 lu::partial_pivoting::{factor, solve as lu_solve},
34 matmul::matmul,
35 svd,
36 },
37 perm::Perm,
38 traits::Conjugate,
39};
40use nalgebra::DMatrix;
41
42use crate::{
43 Complex, Error, Result,
44 numerics::{self, finite, ratio},
45};
46
47pub(crate) fn view(matrix: &DMatrix<Complex>) -> MatRef<'_, Complex> {
49 MatRef::from_column_major_slice(matrix.as_slice(), matrix.nrows(), matrix.ncols())
50}
51
52pub(crate) fn view_mut(matrix: &mut DMatrix<Complex>) -> MatMut<'_, Complex> {
54 let (rows, cols) = matrix.shape();
55 MatMut::from_column_major_slice_mut(matrix.as_mut_slice(), rows, cols)
56}
57
58pub(crate) fn product_into<L, R>(
62 out: MatMut<'_, Complex>,
63 left: MatRef<'_, L>,
64 right: MatRef<'_, R>,
65) where
66 L: Conjugate<Canonical = Complex>,
67 R: Conjugate<Canonical = Complex>,
68{
69 let (m, n, k) = (out.nrows(), out.ncols(), left.ncols());
70 crate::threads::product(m, n, k, |par| {
71 matmul(
72 out,
73 Accum::Replace,
74 left,
75 right,
76 Complex::new(1.0, 0.0),
77 par,
78 );
79 });
80}
81
82pub(crate) fn product_views<L, R>(left: MatRef<'_, L>, right: MatRef<'_, R>) -> DMatrix<Complex>
84where
85 L: Conjugate<Canonical = Complex>,
86 R: Conjugate<Canonical = Complex>,
87{
88 let mut result = DMatrix::zeros(left.nrows(), right.ncols());
89 product_into(view_mut(&mut result), left, right);
90 result
91}
92
93pub(crate) fn product(left: &DMatrix<Complex>, right: &DMatrix<Complex>) -> DMatrix<Complex> {
95 product_views(view(left), view(right))
96}
97
98pub(crate) fn product_adjoint_right(
100 left: &DMatrix<Complex>,
101 right: &DMatrix<Complex>,
102) -> DMatrix<Complex> {
103 product_views(view(left), view(right).adjoint())
104}
105
106pub(crate) fn product_adjoint_right_into(
108 out: &mut DMatrix<Complex>,
109 left: &DMatrix<Complex>,
110 right: &DMatrix<Complex>,
111) {
112 product_into(view_mut(out), view(left), view(right).adjoint());
113}
114
115#[derive(Clone, Debug)]
121pub(crate) struct Equilibration {
122 pub(crate) row: Vec<f64>,
124 pub(crate) column: Vec<f64>,
126}
127
128pub(crate) fn equilibrate(operator: &mut DMatrix<Complex>) -> Result<Option<Equilibration>> {
138 let n = operator.nrows();
139 if n == 0 || !operator.is_square() {
140 return Err(Error::InvalidInput(
141 "require a finite nonempty square operator".into(),
142 ));
143 }
144 let relative_floor = 0.1;
148 let mut minimum = f64::INFINITY;
149 let mut maximum = 0.0_f64;
150 for &z in operator.iter() {
151 if !finite(z) {
152 return Err(Error::InvalidInput(
153 "require a finite nonempty square operator".into(),
154 ));
155 }
156 let magnitude = z.re.abs().max(z.im.abs());
157 if magnitude > 0.0 {
158 minimum = minimum.min(magnitude);
159 }
160 maximum = maximum.max(magnitude);
161 }
162 if maximum > 0.0
163 && (minimum / maximum >= relative_floor
164 || (0..n).all(|i| {
165 let z = operator[(i, i)];
166 z.re.abs().max(z.im.abs()) / maximum >= relative_floor
167 }))
168 {
169 return Ok(None);
170 }
171 let mut row = vec![0.0_f64; n];
174 let mut column_min = f64::INFINITY;
175 for values in operator.as_slice().chunks_exact(n) {
176 let mut column_max = 0.0_f64;
177 for (row_max, &z) in row.iter_mut().zip(values) {
178 let magnitude = z.re.abs().max(z.im.abs());
179 *row_max = row_max.max(magnitude);
180 column_max = column_max.max(magnitude);
181 }
182 column_min = column_min.min(column_max);
183 }
184 let row_min = row.iter().copied().fold(f64::INFINITY, f64::min);
185 if row_min == 0.0 || column_min == 0.0 {
186 return Err(Error::Singular);
187 }
188 if row_min / maximum >= relative_floor && column_min / maximum >= relative_floor {
189 return Ok(None);
190 }
191 if column_min < row_min {
192 operator.transpose_mut();
196 let result = equilibrate(operator);
197 operator.transpose_mut();
198 return result.map(|scales| {
199 scales.map(|scales| Equilibration {
200 row: scales.column,
201 column: scales.row,
202 })
203 });
204 }
205 let inverse = |value: f64| {
206 value
207 .clamp(f64::MIN_POSITIVE, f64::MIN_POSITIVE.recip())
208 .recip()
209 };
210 for value in &mut row {
211 *value = inverse(*value);
212 }
213 let mut column = Vec::with_capacity(n);
214 for values in operator.as_mut_slice().chunks_exact_mut(n) {
215 let mut column_max = 0.0_f64;
216 for (value, &scale) in values.iter_mut().zip(&row) {
217 *value *= scale;
218 column_max = column_max.max(value.re.abs().max(value.im.abs()));
219 }
220 let scale = inverse(column_max);
221 column.push(scale);
222 for value in values {
223 *value *= scale;
224 }
225 }
226 balance(operator, &mut row, &mut column)?;
227 Ok(Some(Equilibration { row, column }))
228}
229
230const BALANCE_SWEEPS: usize = 64;
232
233fn balance(operator: &mut DMatrix<Complex>, row: &mut [f64], column: &mut [f64]) -> Result<()> {
248 let n = operator.nrows();
249 let magnitude = |z: &Complex| z.re.abs().max(z.im.abs());
250 let advance = |factor: &mut f64, sum: f64| {
253 let ratio = sum / std::f64::consts::LN_2;
254 let step = f64::from_bits(ratio.to_bits() & 0x7ff0_0000_0000_0000).recip();
256 *factor *= step;
257 ratio.is_normal().then_some(!(0.5..=2.0).contains(&step))
258 };
259 let mut left = numerics::filled(n, 1.0_f64)?;
260 let mut right = numerics::filled(n, 1.0_f64)?;
261 let mut partial = numerics::filled(n, 0.0_f64)?;
263 for _ in 0..BALANCE_SWEEPS {
264 let mut coarse = false;
265 partial.fill(0.0);
266 for (values, factor) in operator.as_slice().chunks_exact(n).zip(&mut right) {
267 let sum: f64 = values
268 .iter()
269 .zip(&left)
270 .map(|(z, &x)| x * magnitude(z))
271 .sum();
272 let Some(large) = advance(factor, sum * *factor) else {
273 return Ok(());
274 };
275 coarse |= large;
276 for (total, z) in partial.iter_mut().zip(values) {
277 *total += magnitude(z) * *factor;
278 }
279 }
280 for (factor, &total) in left.iter_mut().zip(&partial) {
281 let Some(large) = advance(factor, total * *factor) else {
282 return Ok(());
283 };
284 coarse |= large;
285 }
286 if !coarse {
287 break;
288 }
289 }
290 let bound = 2.0_f64.powi(256);
291 let fits = |scales: &[f64], factors: &[f64]| {
292 scales.iter().zip(factors).all(|(&scale, &factor)| {
293 (bound.recip()..=bound).contains(&factor) && (scale * factor).is_normal()
294 })
295 };
296 if fits(row, &left) && fits(column, &right) {
297 for (values, &y) in operator.as_mut_slice().chunks_exact_mut(n).zip(&right) {
298 for (value, &x) in values.iter_mut().zip(&left) {
300 *value *= x * y;
301 }
302 }
303 for (scales, factors) in [(row, &left), (column, &right)] {
304 for (scale, &factor) in scales.iter_mut().zip(factors) {
305 *scale *= factor;
306 }
307 }
308 }
309 Ok(())
310}
311
312fn scale_rows(mut matrix: MatMut<'_, Complex>, scales: &[f64], shifts: &[i32]) {
317 for (j, &shift) in shifts.iter().enumerate() {
318 for (value, &scale) in matrix.as_mut().col_mut(j).iter_mut().zip(scales) {
319 if shift == 0 {
320 *value *= scale;
321 } else {
322 let (mantissa, exponent) = libm::frexp(scale);
323 let power = exponent + shift;
324 let part = |x: f64| {
325 if power > 0 {
326 libm::scalbn(x, power - 1) * (2.0 * mantissa)
327 } else {
328 libm::scalbn(x * mantissa, power)
329 }
330 };
331 *value = Complex::new(part(value.re), part(value.im));
332 }
333 }
334 }
335}
336
337fn lu_threads(rows: usize, columns: usize, budget: usize) -> usize {
348 if columns < 32 {
352 return 1;
353 }
354 if budget <= 4 {
355 return budget;
356 }
357 (rows / 512)
358 .min(4)
359 .max(rows / 2048)
360 .min(columns / 16)
361 .clamp(1, budget)
362}
363
364fn lu_workers(rows: usize, columns: usize) -> usize {
366 if rows < 64 && columns <= 64 {
370 return 1;
371 }
372 lu_threads(rows, columns, crate::threads::current_num_threads())
373}
374
375fn scratch(req: StackReq) -> Result<MemBuffer> {
378 MemBuffer::try_new(req).map_err(|_| {
379 if req.align_bytes() == 0 {
382 Error::out_of_memory(usize::MAX, 2)
383 } else {
384 Error::out_of_memory(req.size_bytes(), 1)
385 }
386 })
387}
388
389#[derive(Clone, Debug)]
391pub(crate) struct Lu {
392 factors: DMatrix<Complex>,
393 permutation: Perm<usize>,
394 equilibration: Option<Equilibration>,
395}
396
397impl Lu {
398 pub(crate) fn new(mut factors: DMatrix<Complex>) -> Result<Self> {
399 let n = factors.nrows();
400 let equilibration = equilibrate(&mut factors)?;
401 let mut forward = numerics::filled(n, 0_usize)?;
402 let mut inverse = numerics::filled(n, 0_usize)?;
403 crate::threads::dense(lu_workers(n, n), |par| -> Result<()> {
404 factor::lu_in_place(
405 view_mut(&mut factors),
406 &mut forward,
407 &mut inverse,
408 par,
409 MemStack::new(&mut scratch(
410 factor::lu_in_place_scratch::<usize, Complex>(n, n, par, Spec::default()),
411 )?),
412 Spec::default(),
413 );
414 Ok(())
415 })?;
416 if factors.diagonal().iter().any(|&z| z == Complex::default()) {
417 return Err(Error::Singular);
418 }
419 Ok(Self {
420 factors,
421 equilibration,
422 permutation: Perm::new_checked(
423 forward.into_boxed_slice(),
424 inverse.into_boxed_slice(),
425 n,
426 ),
427 })
428 }
429
430 pub(crate) fn solve_in_place(&self, rhs: MatMut<'_, Complex>) -> Result<()> {
432 self.solve_with(rhs, false)
433 }
434
435 pub(crate) fn solve_adjoint_in_place(&self, rhs: MatMut<'_, Complex>) -> Result<()> {
437 self.solve_with(rhs, true)
438 }
439
440 fn solve_with(&self, mut rhs: MatMut<'_, Complex>, adjoint: bool) -> Result<()> {
443 let (n, columns) = (self.factors.nrows(), rhs.ncols());
444 let scales = self.equilibration.as_ref().map(|scales| {
445 if adjoint {
446 (&scales.column, &scales.row)
447 } else {
448 (&scales.row, &scales.column)
449 }
450 });
451 let mut shifts = Vec::new();
458 if let Some((before, _)) = scales {
459 shifts = numerics::filled(columns, 0_i32)?;
460 for (j, shift) in shifts.iter_mut().enumerate() {
461 let entries = || {
463 (rhs.as_ref().col(j).iter().zip(before))
464 .flat_map(|(z, &scale)| [(z.re.abs(), scale), (z.im.abs(), scale)])
465 .filter(|&(magnitude, _)| magnitude > 0.0)
466 };
467 let (smallest, largest) = entries()
469 .map(|(magnitude, scale)| magnitude * scale)
470 .fold((f64::INFINITY, 0.0_f64), |(low, high), x| {
471 (low.min(x), high.max(x))
472 });
473 if largest > 2.0_f64.powi(960) || smallest < 2.0_f64.powi(-1020) {
474 let (low, high) = entries()
476 .map(|(magnitude, scale)| libm::frexp(magnitude).1 + libm::frexp(scale).1)
477 .fold((i32::MAX, i32::MIN), |(low, high), e| {
478 (low.min(e), high.max(e))
479 });
480 let (floor, ceiling) = (-1020 - low, 960 - high);
481 let (lowest, highest) = (-1072 - low, 1023 - high);
482 *shift = if floor <= ceiling {
483 0.clamp(floor, ceiling)
484 } else if lowest <= highest {
485 0.clamp(lowest, highest)
486 } else {
487 0
488 };
489 }
490 }
491 scale_rows(rhs.as_mut(), before, &shifts);
492 }
493 let (factors, permutation) = (view(&self.factors), self.permutation.as_ref());
494 crate::threads::dense(lu_workers(n, columns), |par| -> Result<()> {
495 if adjoint {
496 lu_solve::solve_transpose_in_place_with_conj(
497 factors,
498 factors,
499 permutation,
500 Conj::Yes,
501 rhs.as_mut(),
502 par,
503 MemStack::new(&mut scratch(lu_solve::solve_transpose_in_place_scratch::<
504 usize,
505 Complex,
506 >(n, columns, par))?),
507 );
508 } else {
509 lu_solve::solve_in_place(
510 factors,
511 factors,
512 permutation,
513 rhs.as_mut(),
514 par,
515 MemStack::new(&mut scratch(lu_solve::solve_in_place_scratch::<
516 usize,
517 Complex,
518 >(n, columns, par))?),
519 );
520 }
521 Ok(())
522 })?;
523 if let Some((_, after)) = scales {
524 for shift in &mut shifts {
525 *shift = -*shift;
526 }
527 scale_rows(rhs, after, &shifts);
528 }
529 Ok(())
530 }
531}
532
533#[derive(Clone, Debug)]
536pub struct SolveResidual {
537 lu: Lu,
538 value: DMatrix<Complex>,
539}
540
541pub fn solve(operator: &DMatrix<Complex>, rhs: DMatrix<Complex>) -> Result<SolveResidual> {
543 solve_owned(operator.clone(), rhs)
544}
545
546pub fn solve_owned(operator: DMatrix<Complex>, mut rhs: DMatrix<Complex>) -> Result<SolveResidual> {
548 let n = operator.nrows();
549 if n == 0
550 || !operator.is_square()
551 || rhs.nrows() != n
552 || rhs.ncols() == 0
553 || rhs.iter().any(|&z| !finite(z))
554 {
555 return Err(Error::InvalidInput(
556 "require a finite nonempty square operator and matching right-hand sides".into(),
557 ));
558 }
559 let lu = Lu::new(operator)?;
560 lu.solve_in_place(view_mut(&mut rhs))?;
561 if rhs.iter().any(|&z| !finite(z)) {
562 return Err(Error::Singular);
563 }
564 Ok(SolveResidual { lu, value: rhs })
565}
566
567const DECOMPOSITION: faer::Par = faer::Par::Seq;
573
574#[derive(Debug)]
577struct ThinSvd {
578 u: Mat<Complex>,
579 s: Diag<Complex>,
580 v: Mat<Complex>,
581}
582
583fn faer_zeros(rows: usize, columns: usize) -> Result<Mat<Complex>> {
585 let mut matrix = Mat::new();
586 matrix
587 .try_reserve(rows, columns)
588 .map_err(|_| Error::out_of_memory(rows.saturating_mul(columns), size_of::<Complex>()))?;
589 matrix.resize_with(rows, columns, |_, _| Complex::default());
590 Ok(matrix)
591}
592
593fn thin_svd(a: MatRef<'_, Complex>) -> Result<ThinSvd> {
594 let (m, n) = a.shape();
595 let size = m.min(n);
596 let mut factors = ThinSvd {
597 u: faer_zeros(m, size)?,
598 s: Diag::zeros(size),
599 v: faer_zeros(n, size)?,
600 };
601 let thin = svd::ComputeSvdVectors::Thin;
602 let par = DECOMPOSITION;
603 svd::svd(
604 a,
605 factors.s.as_mut(),
606 Some(factors.u.as_mut()),
607 Some(factors.v.as_mut()),
608 par,
609 MemStack::new(&mut scratch(svd::svd_scratch::<Complex>(
610 m,
611 n,
612 thin,
613 thin,
614 par,
615 Spec::default(),
616 ))?),
617 Spec::default(),
618 )
619 .map_err(|err| Error::NotConverged(format!("singular-value decomposition: {err:?}")))?;
620 Ok(factors)
621}
622
623fn eigen(a: MatRef<'_, Complex>) -> Result<(Diag<Complex>, Mat<Complex>)> {
625 let n = a.nrows();
626 let mut values = Diag::zeros(n);
627 let mut vectors = faer_zeros(n, n)?;
628 let par = DECOMPOSITION;
629 evd::evd_cplx(
630 a,
631 values.as_mut(),
632 None,
633 Some(vectors.as_mut()),
634 par,
635 MemStack::new(&mut scratch(evd::evd_scratch::<Complex>(
636 n,
637 evd::ComputeEigenvectors::No,
638 evd::ComputeEigenvectors::Yes,
639 par,
640 Spec::default(),
641 ))?),
642 Spec::default(),
643 )
644 .map_err(|err| Error::NotConverged(format!("eigendecomposition: {err:?}")))?;
645 Ok((values, vectors))
646}
647
648#[derive(Debug)]
651pub struct SvdvalsResidual {
652 u: Mat<Complex>,
653 v: Mat<Complex>,
654 values: Vec<f64>,
655}
656
657pub fn svdvals(operator: &DMatrix<Complex>) -> Result<SvdvalsResidual> {
659 if operator.is_empty() || operator.iter().any(|&z| !finite(z)) {
660 return Err(Error::InvalidInput(
661 "SVD requires a finite nonempty matrix".into(),
662 ));
663 }
664 let scale = operator.iter().map(|z| z.norm()).fold(0.0, f64::max);
665 let scale = if scale == 0.0 { 1.0 } else { scale };
666 let decomposition = thin_svd(view(&operator.map(|z| z / scale)))?;
667 let values: Vec<_> = (0..operator.nrows().min(operator.ncols()))
668 .map(|i| decomposition.s.as_ref()[i].re * scale)
669 .collect();
670 if values.iter().any(|x| !x.is_finite()) {
671 return Err(Error::NonFinite("non-finite singular values".into()));
672 }
673 Ok(SvdvalsResidual {
674 u: decomposition.u,
675 v: decomposition.v,
676 values,
677 })
678}
679
680impl SvdvalsResidual {
681 #[must_use]
683 pub fn values(&self) -> &[f64] {
684 &self.values
685 }
686
687 #[must_use]
689 pub fn shape(&self) -> (usize, usize) {
690 (self.u.nrows(), self.v.nrows())
691 }
692
693 pub fn pushforward(&self, operator: &DMatrix<Complex>) -> Result<Vec<f64>> {
701 if operator.shape() != self.shape() || operator.iter().any(|&z| !finite(z)) {
702 return Err(Error::InvalidInput(
703 "invalid singular-value operator tangent".into(),
704 ));
705 }
706 let tolerance = 64.0 * f64::EPSILON * self.values[0];
707 if self.values.iter().any(|&value| value <= tolerance) {
708 return Err(Error::InvalidInput(
709 "singular values at zero or below numerical resolution have no pushforward".into(),
710 ));
711 }
712 if self
713 .values
714 .windows(2)
715 .any(|pair| pair[0] - pair[1] <= tolerance)
716 {
717 return Err(Error::InvalidInput(
718 "individual repeated singular values have no pushforward".into(),
719 ));
720 }
721 let moved = product_views(view(operator), self.v.as_ref());
722 let u = self.u.as_ref();
723 Ok((0..self.values.len())
724 .map(|j| {
725 (0..u.nrows())
726 .map(|i| (u[(i, j)].conj() * moved[(i, j)]).re)
727 .sum()
728 })
729 .collect())
730 }
731
732 pub fn pullback(&self, cotangent: &[f64]) -> Result<DMatrix<Complex>> {
736 if cotangent.len() != self.values.len() || cotangent.iter().any(|x| !x.is_finite()) {
737 return Err(Error::Derivative(
738 crate::DerivativeError::InvalidSingularValueCotangent,
739 ));
740 }
741 let tolerance = 64.0 * f64::EPSILON * self.values[0];
742 let weight_tolerance =
743 64.0 * f64::EPSILON * cotangent.iter().map(|x| x.abs()).fold(0.0, f64::max);
744 for i in 0..cotangent.len() {
745 if self.values[i] <= tolerance && cotangent[i].abs() > weight_tolerance {
746 return Err(Error::Derivative(
747 crate::DerivativeError::UnresolvedSingularValue,
748 ));
749 }
750 if i > 0
751 && self.values[i - 1] > tolerance
752 && (self.values[i - 1] - self.values[i]).abs() <= tolerance
753 && (cotangent[i - 1] - cotangent[i]).abs()
754 > 64.0 * f64::EPSILON * cotangent[i - 1].abs().max(cotangent[i].abs())
755 {
756 return Err(Error::Derivative(
757 crate::DerivativeError::UnequalSingularValueWeights,
758 ));
759 }
760 }
761 let u = self.u.as_ref();
762 let weighted =
763 DMatrix::from_fn(u.nrows(), cotangent.len(), |i, j| u[(i, j)] * cotangent[j]);
764 Ok(product_views(view(&weighted), self.v.as_ref().adjoint()))
765 }
766}
767
768#[derive(Clone, Debug)]
771pub struct SolveGradient {
772 pub operator: DMatrix<Complex>,
774 pub rhs: DMatrix<Complex>,
776}
777
778impl SolveResidual {
779 #[must_use]
782 pub const fn value(&self) -> &DMatrix<Complex> {
783 &self.value
784 }
785
786 #[must_use]
788 pub fn shape(&self) -> (usize, usize) {
789 self.value.shape()
790 }
791
792 pub fn pushforward(
794 &self,
795 operator: &DMatrix<Complex>,
796 mut rhs: DMatrix<Complex>,
797 ) -> Result<DMatrix<Complex>> {
798 let n = self.value.nrows();
799 if operator.shape() != (n, n)
800 || rhs.shape() != self.shape()
801 || operator.iter().chain(rhs.iter()).any(|&z| !finite(z))
802 {
803 return Err(Error::InvalidInput("invalid linear-solve tangent".into()));
804 }
805 rhs -= product(operator, &self.value);
806 self.lu.solve_in_place(view_mut(&mut rhs))?;
807 if rhs.iter().any(|&z| !finite(z)) {
808 return Err(Error::Singular);
809 }
810 Ok(rhs)
811 }
812
813 pub(crate) fn adjoint_rhs(&self, mut cotangent: DMatrix<Complex>) -> Result<DMatrix<Complex>> {
814 if cotangent.shape() != self.shape() || cotangent.iter().any(|&z| !finite(z)) {
815 return Err(Error::InvalidInput("invalid linear-solve cotangent".into()));
816 }
817 self.lu.solve_adjoint_in_place(view_mut(&mut cotangent))?;
818 Ok(cotangent)
819 }
820
821 pub fn pullback(&self, cotangent: DMatrix<Complex>) -> Result<SolveGradient> {
823 let rhs = self.adjoint_rhs(cotangent)?;
824 Ok(SolveGradient {
825 operator: -product_adjoint_right(&rhs, &self.value),
826 rhs,
827 })
828 }
829}
830
831#[derive(Debug)]
834pub struct EigResidual {
835 values: Vec<Complex>,
836 vectors: DMatrix<Complex>,
837 pivots: Vec<usize>,
838 scale: f64,
839 vectors_lu: OnceLock<Result<Lu>>,
842}
843
844pub fn eig(operator: &DMatrix<Complex>) -> Result<EigResidual> {
846 let n = operator.nrows();
847 if n == 0 || !operator.is_square() || operator.iter().any(|&z| !finite(z)) {
848 return Err(Error::InvalidInput(
849 "eigensystem requires a finite nonempty square matrix".into(),
850 ));
851 }
852 let scale = operator.iter().map(|z| z.norm()).fold(0.0, f64::max);
853 let scale = if scale == 0.0 { 1.0 } else { scale };
854 let (eigenvalues, eigenvectors) = eigen(view(&operator.map(|z| z / scale)))?;
855 let values: Vec<_> = (0..n).map(|i| eigenvalues.as_ref()[i] * scale).collect();
856 let mut vectors = DMatrix::from_fn(n, n, |i, j| eigenvectors[(i, j)]);
857 let mut pivots = Vec::with_capacity(n);
858 for mut column in vectors.column_iter_mut() {
859 let pivot = (0..n)
860 .max_by(|&i, &j| column[i].norm_sqr().total_cmp(&column[j].norm_sqr()))
861 .ok_or(Error::Singular)?;
862 let phase = column[pivot] / column[pivot].norm();
863 let normalization = phase * column.norm();
864 column /= normalization;
865 pivots.push(pivot);
866 }
867 if values.iter().chain(vectors.iter()).any(|&z| !finite(z)) {
868 return Err(Error::NonFinite("non-finite eigensystem".into()));
869 }
870 Ok(EigResidual {
871 values,
872 vectors,
873 pivots,
874 scale,
875 vectors_lu: OnceLock::new(),
876 })
877}
878
879pub fn eigvals(operator: &DMatrix<Complex>) -> Result<EigResidual> {
882 let mut residual = eig(operator)?;
883 let n = residual.values.len();
884 let mut order: Vec<_> = (0..n).collect();
885 order.sort_by(|&i, &j| {
886 residual.values[i]
887 .re
888 .total_cmp(&residual.values[j].re)
889 .then_with(|| residual.values[i].im.total_cmp(&residual.values[j].im))
890 });
891 residual.values = order.iter().map(|&j| residual.values[j]).collect();
892 residual.vectors = DMatrix::from_fn(n, n, |i, j| residual.vectors[(i, order[j])]);
893 residual.pivots = order.iter().map(|&j| residual.pivots[j]).collect();
894 Ok(residual)
895}
896
897impl EigResidual {
898 fn vectors_lu(&self) -> Result<&Lu> {
899 self.vectors_lu
900 .get_or_init(|| Lu::new(self.vectors.clone()))
901 .as_ref()
902 .map_err(Clone::clone)
903 }
904
905 #[must_use]
907 pub fn values(&self) -> &[Complex] {
908 &self.values
909 }
910
911 #[must_use]
913 pub const fn vectors(&self) -> &DMatrix<Complex> {
914 &self.vectors
915 }
916
917 fn tied_pivot(&self, column: usize) -> bool {
919 let pivot = self.pivots[column];
920 let size = self.vectors[(pivot, column)].norm();
921 (0..self.values.len()).any(|i| {
922 i != pivot
923 && (size - self.vectors[(i, column)].norm()).abs() <= 64.0 * f64::EPSILON * size
924 })
925 }
926
927 fn eigenbasis_tangent(&self, operator: &DMatrix<Complex>) -> Result<DMatrix<Complex>> {
930 let n = self.values.len();
931 if operator.shape() != (n, n) || operator.iter().any(|&z| !finite(z)) {
932 return Err(Error::InvalidInput("invalid eigensystem tangent".into()));
933 }
934 for j in 0..n {
935 for i in 0..j {
936 if (self.values[j] - self.values[i]).norm() <= 64.0 * f64::EPSILON * self.scale {
937 return Err(Error::InvalidInput(
938 "individual eigenmodes have no pushforward at repeated eigenvalues".into(),
939 ));
940 }
941 }
942 }
943 let mut moved = product(operator, &self.vectors);
944 self.vectors_lu()?.solve_in_place(view_mut(&mut moved))?;
945 if moved.iter().any(|&z| !finite(z)) {
946 return Err(Error::Singular);
947 }
948 Ok(moved)
949 }
950
951 fn check_value_order(&self, weights: Option<&[Complex]>) -> Result<()> {
954 for j in 0..self.values.len() {
955 for i in 0..j {
956 let tied = (self.values[j].re - self.values[i].re).abs()
957 <= 64.0 * f64::EPSILON * self.scale;
958 let equal_weights = weights.is_some_and(|g| {
959 (g[j] - g[i]).norm() <= 64.0 * f64::EPSILON * g[j].norm().max(g[i].norm())
960 });
961 if tied && !equal_weights {
962 return Err(Error::InvalidInput(
963 "ordered eigenvalue derivatives require distinct real parts or equal loss weights at ordering ties".into(),
964 ));
965 }
966 }
967 }
968 Ok(())
969 }
970
971 pub fn pushforward_values(&self, operator: &DMatrix<Complex>) -> Result<Vec<Complex>> {
974 let moved = self.eigenbasis_tangent(operator)?;
975 self.check_value_order(None)?;
976 Ok(moved.diagonal().iter().copied().collect())
977 }
978
979 pub fn pullback_values(&self, values: &[Complex]) -> Result<DMatrix<Complex>> {
981 let n = self.values.len();
982 let gradient = self.pullback(values, DMatrix::zeros(n, n))?;
983 self.check_value_order(Some(values))?;
984 Ok(gradient)
985 }
986
987 pub fn pushforward(
992 &self,
993 operator: &DMatrix<Complex>,
994 ) -> Result<(Vec<Complex>, DMatrix<Complex>)> {
995 let n = self.values.len();
996 let mut moved = self.eigenbasis_tangent(operator)?;
997 if (0..n).any(|j| self.tied_pivot(j)) {
998 return Err(Error::InvalidInput(
999 "eigenvector pushforward requires a unique largest component; use eigvals for eigenvalues alone".into(),
1000 ));
1001 }
1002 let values: Vec<_> = moved.diagonal().iter().copied().collect();
1003 for j in 0..n {
1006 for i in 0..n {
1007 moved[(i, j)] = if i == j {
1008 Complex::default()
1009 } else {
1010 ratio(moved[(i, j)], self.values[j] - self.values[i])
1011 };
1012 }
1013 }
1014 let mut vectors = product(&self.vectors, &moved);
1015 for j in 0..n {
1016 let pivot = self.pivots[j];
1017 let normalization = self.vectors.column(j).dotc(&vectors.column(j)).re;
1018 let phase = vectors[(pivot, j)].im / self.vectors[(pivot, j)].re;
1019 let correction = Complex::new(normalization, phase);
1020 for i in 0..n {
1021 vectors[(i, j)] -= self.vectors[(i, j)] * correction;
1022 }
1023 }
1024 if values.iter().chain(vectors.iter()).any(|&z| !finite(z)) {
1025 return Err(Error::Singular);
1026 }
1027 Ok((values, vectors))
1028 }
1029
1030 pub fn pullback(
1036 &self,
1037 values: &[Complex],
1038 mut vectors: DMatrix<Complex>,
1039 ) -> Result<DMatrix<Complex>> {
1040 let n = self.values.len();
1041 if values.len() != n
1042 || vectors.shape() != (n, n)
1043 || values.iter().chain(vectors.iter()).any(|&z| !finite(z))
1044 {
1045 return Err(Error::InvalidInput("invalid eigensystem cotangent".into()));
1046 }
1047 let vector_active: Vec<_> = vectors
1048 .column_iter()
1049 .map(|v| v.iter().any(|&z| z != Complex::default()))
1050 .collect();
1051 if !vector_active.iter().any(|&active| active) && values.iter().all(|&g| g == values[0]) {
1052 return Ok(DMatrix::identity(n, n) * values[0]);
1054 }
1055 for j in 0..n {
1058 let inner = vectors.column(j).dotc(&self.vectors.column(j));
1059 let pivot = self.pivots[j];
1060 if inner.im.abs() > 64.0 * f64::EPSILON * vectors.column(j).norm() && self.tied_pivot(j)
1061 {
1062 return Err(Error::InvalidInput(
1063 "phase-dependent eigenvector pullback requires a unique largest component"
1064 .into(),
1065 ));
1066 }
1067 for i in 0..n {
1068 vectors[(i, j)] -= inner.re * self.vectors[(i, j)];
1069 }
1070 vectors[(pivot, j)] += Complex::i() * inner.im / self.vectors[(pivot, j)].re;
1071 }
1072 let mut g = product_views(view(&self.vectors).adjoint(), view(&vectors));
1073 for j in 0..n {
1074 for i in 0..n {
1075 if i == j {
1076 g[(i, j)] = values[i];
1077 continue;
1078 }
1079 let gap = self.values[j] - self.values[i];
1080 if gap.norm() <= 64.0 * f64::EPSILON * self.scale {
1081 let weight_scale = values[i].norm().max(values[j].norm());
1082 if vector_active[i]
1083 || vector_active[j]
1084 || (values[i] - values[j]).norm() > 64.0 * f64::EPSILON * weight_scale
1085 {
1086 return Err(Error::InvalidInput(
1087 "individual eigenmodes have no pullback at repeated eigenvalues".into(),
1088 ));
1089 }
1090 g[(i, j)] = Complex::default();
1091 } else {
1092 g[(i, j)] = ratio(g[(i, j)], gap.conj());
1093 }
1094 }
1095 }
1096 let mut result = product_adjoint_right(&g, &self.vectors);
1097 self.vectors_lu()?
1098 .solve_adjoint_in_place(view_mut(&mut result))?;
1099 if result.iter().any(|&z| !finite(z)) {
1100 return Err(Error::Singular);
1101 }
1102 Ok(result)
1103 }
1104}
1105
1106#[cfg(test)]
1108mod tests {
1109 use faer::dyn_stack::StackReq;
1110 use nalgebra::DMatrix;
1111 use proptest::{prelude::*, test_runner::TestCaseError};
1112
1113 use super::{SolveGradient, eig, equilibrate, faer_zeros, lu_threads, scratch, solve_owned};
1114 use crate::{
1115 Complex, Error,
1116 test_support::{ALGEBRA_CASES, complex_matrix, prop_assert_close},
1117 };
1118
1119 #[test]
1120 fn eigen_derivatives_share_one_lazy_factorization() {
1121 let operator = DMatrix::from_diagonal(&nalgebra::DVector::from_vec(vec![
1122 Complex::new(2.0, 0.1),
1123 Complex::new(3.0, -0.2),
1124 ]));
1125 let residual = eig(&operator).unwrap();
1126 assert!(residual.vectors_lu.get().is_none());
1127 let weights = [Complex::from(1.0); 2];
1130 assert_eq!(
1131 residual.pullback(&weights, DMatrix::zeros(2, 2)).unwrap(),
1132 DMatrix::identity(2, 2)
1133 );
1134 assert!(residual.vectors_lu.get().is_none());
1135 let direction = DMatrix::from_element(2, 2, Complex::new(0.2, 0.1));
1136 let tangent = residual.pushforward(&direction).unwrap();
1137 let factor = residual.vectors_lu().unwrap();
1138 let gradient = residual
1139 .pullback(&weights, DMatrix::from_element(2, 2, Complex::from(0.3)))
1140 .unwrap();
1141 assert_eq!(residual.pushforward(&direction).unwrap(), tangent);
1142 assert_eq!(
1143 residual
1144 .pullback(&weights, DMatrix::from_element(2, 2, Complex::from(0.3)))
1145 .unwrap(),
1146 gradient
1147 );
1148 assert!(std::ptr::eq(factor, residual.vectors_lu().unwrap()));
1149 }
1150
1151 proptest! {
1152 #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
1153
1154 #[test]
1155 fn lu_scheduling_respects_worker_and_rhs_budgets(
1156 rows in 1_usize..100_000,
1157 columns in 1_usize..100_000,
1158 budget in 1_usize..128,
1159 ) {
1160 check_lu_scheduling(rows, columns, budget)?;
1161 }
1162 }
1163
1164 fn check_lu_scheduling(
1167 rows: usize,
1168 columns: usize,
1169 budget: usize,
1170 ) -> Result<(), TestCaseError> {
1171 let threads = lu_threads(rows, columns, budget);
1172 prop_assert!((1..=budget).contains(&threads));
1173 if columns < 32 {
1174 prop_assert_eq!(threads, 1);
1175 } else if budget <= 4 {
1176 prop_assert_eq!(threads, budget);
1177 }
1178 Ok(())
1179 }
1180
1181 #[derive(Clone, Debug)]
1183 enum Units {
1184 Rows(Vec<i32>),
1186 Columns(Vec<i32>),
1188 Both(Vec<i32>, Vec<i32>),
1190 Alternating { exponent: i32, transposed: bool },
1193 }
1194
1195 impl Units {
1196 fn scales(&self, n: usize) -> (Vec<f64>, Vec<f64>) {
1198 let power = |e: i32| 2.0_f64.powi(e);
1199 let unit = vec![1.0; n];
1200 match self {
1201 Self::Rows(e) => (e.iter().map(|&e| power(e)).collect(), unit),
1202 Self::Columns(e) => (unit, e.iter().map(|&e| power(e)).collect()),
1203 Self::Both(r, c) => (
1204 r.iter().map(|&e| power(e)).collect(),
1205 c.iter().map(|&e| power(e)).collect(),
1206 ),
1207 &Self::Alternating {
1208 exponent,
1209 transposed,
1210 } => {
1211 let sign = |i: usize| if i.is_multiple_of(2) { -1 } else { 1 };
1212 let (wide, narrow) = (exponent, exponent / 2);
1213 let (row, column) = if transposed {
1214 (narrow, wide)
1215 } else {
1216 (wide, narrow)
1217 };
1218 (
1219 (0..n).map(|i| power(sign(i) * row)).collect(),
1220 (0..n).map(|j| power(-sign(j) * column)).collect(),
1221 )
1222 }
1223 }
1224 }
1225 }
1226
1227 fn system() -> impl Strategy<Value = (DMatrix<Complex>, Units, DMatrix<Complex>)> {
1228 (1_usize..=40, 1_usize..=3).prop_flat_map(|(n, columns)| {
1229 let exponents = || prop::collection::vec(-400_i32..=400, n);
1230 (
1231 complex_matrix(n, n, 0.5),
1232 Just((0..n).collect::<Vec<_>>()).prop_shuffle(),
1233 prop_oneof![
1234 exponents().prop_map(Units::Rows),
1235 exponents().prop_map(Units::Columns),
1236 (exponents(), exponents()).prop_map(|(r, c)| Units::Both(r, c)),
1237 (0_i32..=400, any::<bool>()).prop_map(|(exponent, transposed)| {
1238 Units::Alternating {
1239 exponent,
1240 transposed,
1241 }
1242 }),
1243 ],
1244 complex_matrix(n, columns, 1.0),
1245 )
1246 .prop_map(move |(random, rows, units, y)| {
1247 let size = Complex::from(f64::from(u32::try_from(n).unwrap()));
1249 let mut h = random + DMatrix::identity(n, n) * size;
1250 h = DMatrix::from_fn(n, n, |i, j| h[(rows[i], j)]);
1251 (h, units, y)
1252 })
1253 })
1254 }
1255
1256 proptest! {
1257 #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
1258
1259 #[test]
1260 fn solve_and_adjoint_are_invariant_to_equation_and_unknown_units(
1261 (h, units, y) in system(),
1262 ) {
1263 check_units(&h, &units, &y)?;
1264 }
1265 }
1266
1267 #[test]
1271 fn units_keep_the_pivots_of_a_dominant_diagonal() -> Result<(), TestCaseError> {
1272 let t = |k: usize| f64::from(u32::try_from(k).unwrap());
1273 let h = DMatrix::from_fn(6, 6, |i, j| {
1274 Complex::new(0.5 - 0.125 * t(i), 0.25 + 0.125 * t(j))
1275 }) + DMatrix::identity(6, 6) * Complex::from(2.0_f64.powi(20));
1276 let y = DMatrix::from_fn(6, 1, |i, _| Complex::new(1.0, t(i)));
1277 for transposed in [false, true] {
1278 let units = Units::Alternating {
1279 exponent: 64,
1280 transposed,
1281 };
1282 check_units(&h, &units, &y)?;
1283 }
1284 Ok(())
1285 }
1286
1287 fn check_units(
1294 h: &DMatrix<Complex>,
1295 units: &Units,
1296 y: &DMatrix<Complex>,
1297 ) -> Result<(), TestCaseError> {
1298 let n = h.nrows();
1299 let (row, column) = units.scales(n);
1300 let operator = DMatrix::from_fn(n, n, |i, j| h[(i, j)] * row[i] * column[j]);
1301 let hy = h * y;
1302 let rhs = DMatrix::from_fn(n, y.ncols(), |i, j| hy[(i, j)] * row[i]);
1303 let tolerance = 1e-13 * y.norm();
1304 let residual = solve_owned(operator.clone(), rhs.clone()).unwrap();
1305 let unknowns = DMatrix::from_fn(n, y.ncols(), |i, j| residual.value[(i, j)] * column[i]);
1306 prop_assert_close!(&unknowns, y, tolerance);
1307 let hz = h.adjoint() * y;
1308 let cotangent = DMatrix::from_fn(n, y.ncols(), |i, j| hz[(i, j)] * column[i]);
1309 let adjoint = residual.adjoint_rhs(cotangent.clone()).unwrap();
1310 let equations = DMatrix::from_fn(n, y.ncols(), |i, j| adjoint[(i, j)] * row[i]);
1311 prop_assert_close!(&equations, y, tolerance);
1312
1313 let SolveGradient {
1314 operator: ga,
1315 rhs: gb,
1316 } = residual.pullback(cotangent).unwrap();
1317 prop_assert_eq!(&gb, &adjoint);
1318 for i in 0..n {
1319 for j in 0..n {
1320 let terms = (0..y.ncols()).map(|p| (adjoint[(i, p)], residual.value[(j, p)]));
1321 let expected: Complex = terms.clone().map(|(z, x)| -z * x.conj()).sum();
1322 let scale: f64 = terms.map(|(z, x)| z.norm() * x.norm()).sum();
1323 prop_assert_close!(ga[(i, j)], expected, 1e-14 * scale, "entry ({}, {})", i, j);
1324 }
1325 }
1326 let pairs = ga.iter().zip(&operator).chain(gb.iter().zip(&rhs));
1328 let magnitude: f64 = pairs.clone().map(|(g, x)| g.norm() * x.norm()).sum();
1329 let joint: f64 = pairs.map(|(g, x)| (g.conj() * x).re).sum();
1330 prop_assert_close!(joint, 0.0, 1e-13 * magnitude);
1331
1332 let Units::Alternating {
1334 exponent,
1335 transposed,
1336 } = *units
1337 else {
1338 return Ok(());
1339 };
1340 if n < 2 || exponent < 32 {
1342 return Ok(());
1343 }
1344 let magnitude = |z: Complex| z.re.abs().max(z.im.abs());
1345 let row_min = operator
1346 .row_iter()
1347 .map(|r| r.iter().copied().map(magnitude).fold(0.0, f64::max))
1348 .fold(f64::INFINITY, f64::min);
1349 let column_min = operator
1350 .column_iter()
1351 .map(|c| c.iter().copied().map(magnitude).fold(0.0, f64::max))
1352 .fold(f64::INFINITY, f64::min);
1353 prop_assert_eq!(column_min < row_min, transposed);
1354 let mut equilibrated = operator.clone();
1355 let scales = equilibrate(&mut equilibrated).unwrap().unwrap();
1356 for i in 0..n {
1357 for j in 0..n {
1358 let expected = operator[(i, j)] * scales.row[i] * scales.column[j];
1359 let actual = equilibrated[(i, j)];
1360 let tolerance = 4.0 * f64::EPSILON * expected.norm() + f64::MIN_POSITIVE;
1361 prop_assert_close!(actual, expected, tolerance, "entry ({}, {})", i, j);
1362 }
1363 }
1364 let mut transpose = operator.transpose();
1365 let swapped = equilibrate(&mut transpose).unwrap().unwrap();
1366 prop_assert_eq!(&scales.row, &swapped.column);
1367 prop_assert_eq!(&scales.column, &swapped.row);
1368 Ok(())
1369 }
1370
1371 #[test]
1372 fn scaled_solves_keep_solutions_near_the_float_limits() {
1373 let p = |e: i32| 2.0_f64.powi(e);
1374 let (h, t, u) = (p(1023), p(-100), p(-1022));
1375 let systems = [
1376 (
1378 4,
1379 vec![
1380 1.0, 1.0, 0.0, 0.0, 0.0, t, 0.0, 0.0, 0.0, 0.0, 1.0, u, 0.0, 0.0, 1.0, -u,
1381 ],
1382 vec![0.0, h, 0.0, 1.0],
1383 ),
1384 (2, vec![0.5, 0.5, 0.0, t], vec![1.5 * h, 1.5 * h]),
1386 (2, vec![1.0, 0.0, 0.0, p(-1000)], vec![p(1000), 1.0]),
1388 (2, vec![t, 0.0, 0.0, 1.0], vec![p(-974), h]),
1390 ];
1391 let check = |a: DMatrix<Complex>, x: DMatrix<Complex>| {
1392 let b = &a * &x;
1393 assert_eq!(solve_owned(a.clone(), b.clone()).unwrap().value, x);
1394 let transposed = solve_owned(a.adjoint(), DMatrix::zeros(x.nrows(), 1)).unwrap();
1395 assert_eq!(transposed.adjoint_rhs(b).unwrap(), x);
1396 };
1397 for (n, a, x) in systems {
1398 let a = DMatrix::from_row_slice(n, n, &a).map(Complex::from);
1399 check(a, DMatrix::from_vec(n, 1, x).map(Complex::from));
1400 }
1401 let a = DMatrix::from_row_slice(2, 2, &[1.0, u, 1.0, -u]).map(Complex::from);
1403 check(
1404 a,
1405 DMatrix::from_vec(2, 1, vec![Complex::from(h), Complex::i()]),
1406 );
1407 }
1408
1409 #[test]
1410 fn refused_and_overflowing_workspaces_return_out_of_memory() {
1411 let more = format!("cannot allocate more than {} bytes", usize::MAX);
1412 let half = StackReq::new::<Complex>(1 << 59);
1413 for overflow in [StackReq::new::<Complex>(usize::MAX), half.and(half)] {
1414 assert!(
1415 matches!(scratch(overflow), Err(Error::OutOfMemory(message)) if message == more),
1416 "{overflow:?}"
1417 );
1418 }
1419 assert!(matches!(
1421 scratch(StackReq::new::<Complex>(1 << 48)),
1422 Err(Error::OutOfMemory(message)) if message == "cannot allocate 4503599627370496 bytes"
1423 ));
1424 assert!(scratch(StackReq::new::<Complex>(4)).is_ok());
1425 assert!(matches!(
1426 faer_zeros(1 << 24, 1 << 24),
1427 Err(Error::OutOfMemory(_))
1428 ));
1429 assert!(matches!(
1430 faer_zeros(usize::MAX, 2),
1431 Err(Error::OutOfMemory(_))
1432 ));
1433 let matrix = faer_zeros(3, 2).unwrap();
1434 assert_eq!(matrix.shape(), (3, 2));
1435 assert_eq!(matrix[(2, 1)], Complex::default());
1436 }
1437
1438 #[test]
1439 fn ordinary_operator_is_not_modified() {
1440 let mut operator = DMatrix::from_row_slice(
1441 2,
1442 2,
1443 &[
1444 Complex::new(2.0, 1.0),
1445 Complex::new(0.5, -0.25),
1446 Complex::new(-0.25, 0.5),
1447 Complex::new(3.0, -1.0),
1448 ],
1449 );
1450 let original = operator.clone();
1451 assert!(equilibrate(&mut operator).unwrap().is_none());
1452 assert_eq!(operator, original);
1453 operator[(0, 1)] = Complex::from(2.0_f64.powi(-100));
1454 assert!(equilibrate(&mut operator).unwrap().is_none());
1455 operator *= Complex::from(2.0_f64.powi(-600));
1456 assert!(equilibrate(&mut operator).unwrap().is_none());
1457 }
1458
1459 #[test]
1460 fn extreme_column_units_do_not_underflow_during_row_scaling() {
1461 let high = 2.0_f64.powi(600);
1462 let low = high.recip();
1463 let operator = DMatrix::from_row_slice(
1464 2,
1465 2,
1466 &[
1467 Complex::from(high),
1468 Complex::from(low),
1469 Complex::from(high),
1470 Complex::from(2.0 * low),
1471 ],
1472 );
1473 let rhs = DMatrix::from_column_slice(2, 1, &[Complex::from(1.0), Complex::from(2.0)]);
1474 let residual = solve_owned(operator, rhs).unwrap();
1475 assert_eq!(residual.value[(0, 0)], Complex::default());
1476 assert_eq!(residual.value[(1, 0)], Complex::from(high));
1477 let cotangent =
1479 DMatrix::from_column_slice(2, 1, &[Complex::default(), Complex::from(-low)]);
1480 let adjoint = residual.adjoint_rhs(cotangent).unwrap();
1481 assert_eq!(adjoint[(0, 0)], Complex::from(1.0));
1482 assert_eq!(adjoint[(1, 0)], Complex::from(-1.0));
1483 }
1484}