faer/linalg/evd/balance.rs
1use super::*;
2use crate::assert;
3use crate::perm::{swap_cols_idx, swap_rows_idx};
4
5/// Balancing failed because the input contains a nonfinite entry.
6///
7/// # Examples
8/// ```
9/// use dyn_stack::{MemBuffer, MemStack};
10/// use faer::Par;
11/// use faer::linalg::evd::*;
12/// let mut a = faer::mat![[f64::NAN]];
13/// let mut mem = MemBuffer::new(balance_scratch());
14/// assert_eq!(
15/// balance_in_place(
16/// a.as_mut(),
17/// &mut [0],
18/// &mut [1.0],
19/// Par::Seq,
20/// MemStack::new(&mut mem)
21/// ),
22/// Err(BalanceError::NonFiniteInput)
23/// );
24/// ```
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
26pub enum BalanceError {
27 /// NaN or infinity in the input; the matrix is unchanged.
28 NonFiniteInput,
29}
30
31/// Workspace for [`balance_in_place`]; all metadata is supplied by the caller.
32///
33/// # Examples
34/// ```
35/// assert_eq!(faer::linalg::evd::balance_scratch().size_bytes(), 0);
36/// ```
37pub fn balance_scratch() -> StackReq {
38 StackReq::EMPTY
39}
40
41/// Permutes and balances a square matrix in place, like `xGEBAL(job=B)`.
42/// Returns the zero-based, half-open active range `ilo..ihi`. Outside this
43/// range the permutation isolates eigenvalues in triangular blocks.
44///
45/// On return `b[i,j] = a[perm[i],perm[j]] * scale[j] / scale[i]`:
46/// `perm[i]` is the original index now at row/column `i`, and `scale` holds
47/// positive powers of two (one outside the active range). Scaling is accepted
48/// only if it reduces the sum of the active row/column 2-norms (including the
49/// diagonal) by at least 5%, subject to safe-range guards. This is the LAPACK
50/// 3.5+ criterion, rather than the older 1-norm rule.
51///
52/// Use [`balance_eigenvectors_in_place`] for right eigenvectors. For a matrix
53/// function such as `exp`, undo by `f(a)[perm[i],perm[j]] =
54/// scale[i] * f(b)[i,j] / scale[j]`. Powers of two introduce no rounding when
55/// the scaled values stay normal; underflow can prevent bit-for-bit recovery.
56///
57/// # Panics
58/// Panics unless `a` is square and both metadata slices have length `n`.
59///
60/// # Errors
61/// Returns [`BalanceError::NonFiniteInput`] before modifying any output if an
62/// entry is NaN or infinite.
63///
64/// # Examples
65/// ```
66/// use dyn_stack::{MemBuffer, MemStack};
67/// use faer::Par;
68/// use faer::linalg::evd::*;
69/// let mut a = faer::mat![[1.0f64, 1024.0], [1.0 / 1024.0, 2.0]];
70/// let (mut perm, mut scale) = ([0; 2], [0.0; 2]);
71/// let mut mem = MemBuffer::new(balance_scratch());
72/// let active = balance_in_place(
73/// a.as_mut(),
74/// &mut perm,
75/// &mut scale,
76/// Par::Seq,
77/// MemStack::new(&mut mem),
78/// )
79/// .unwrap();
80/// assert_eq!(active, 0..2);
81/// let row = a.row(0).norm_l2();
82/// let col = a.col(0).norm_l2();
83/// assert!(row <= 2.0 * col && col <= 2.0 * row);
84/// assert_eq!(a[(0, 1)] * scale[0] / scale[1], 1024.0);
85/// ```
86pub fn balance_in_place<T: ComplexField>(
87 mut a: MatMut<'_, T>,
88 perm: &mut [usize],
89 scale: &mut [T::Real],
90 _par: Par,
91 _stack: &mut MemStack,
92) -> Result<core::ops::Range<usize>, BalanceError> {
93 let n = a.nrows();
94 assert!(a.ncols() == n && perm.len() == n && scale.len() == n);
95 if !a.is_all_finite() {
96 return Err(BalanceError::NonFiniteInput);
97 }
98 for i in 0..n {
99 perm[i] = i;
100 scale[i] = one();
101 }
102 let mut lo = 0;
103 let mut hi = n;
104 loop {
105 let row = (lo..hi)
106 .rev()
107 .find(|&i| (lo..hi).all(|j| i == j || a[(i, j)] == zero()));
108 if let Some(i) = row {
109 hi -= 1;
110 swap_rows_idx(a.rb_mut(), i, hi);
111 swap_cols_idx(a.rb_mut(), i, hi);
112 perm.swap(i, hi);
113 continue;
114 }
115 let col =
116 (lo..hi).find(|&j| (lo..hi).all(|i| i == j || a[(i, j)] == zero()));
117 if let Some(j) = col {
118 swap_rows_idx(a.rb_mut(), j, lo);
119 swap_cols_idx(a.rb_mut(), j, lo);
120 perm.swap(j, lo);
121 lo += 1;
122 continue;
123 }
124 break;
125 }
126 // Own implementation of Parlett-Reinsch balancing (Numer. Math. 13,
127 // 293-304, 1969), using the 2-norm convergence rule of LAPACK >= 3.5
128 // xGEBAL (James-Langou-Lowery, arXiv:1401.5766). No LAPACK source is
129 // copied.
130 let two = from_f64::<T::Real>(2.0);
131 let half = from_f64::<T::Real>(0.5);
132 let factor = from_f64::<T::Real>(0.95);
133 let small = min_positive::<T::Real>() / eps::<T::Real>();
134 let large = small.recip();
135 loop {
136 let mut changed = false;
137 for i in lo..hi {
138 let mut c = zero::<T::Real>();
139 let mut r = zero::<T::Real>();
140 for j in lo..hi {
141 c = c.hypot(&a[(j, i)].abs());
142 r = r.hypot(&a[(i, j)].abs());
143 }
144 if c == zero() || r == zero() || !c.is_finite() || !r.is_finite() {
145 continue;
146 }
147 let mut ca = zero::<T::Real>();
148 let mut ra = zero::<T::Real>();
149 for j in 0..n {
150 ca = ca.fmax(a[(j, i)].abs());
151 ra = ra.fmax(a[(i, j)].abs());
152 }
153 let original = c.mul_pow2(half.copy()) + r.mul_pow2(half.copy());
154 let mut f = one::<T::Real>();
155 while c < r.mul_pow2(half.copy())
156 && c < large.mul_pow2(half.copy())
157 && ca < large.mul_pow2(half.copy())
158 && r > &small * &two
159 && ra > &small * &two
160 && f < large.mul_pow2(half.copy())
161 {
162 c *= &two;
163 r *= ½
164 ca *= &two;
165 ra *= ½
166 f *= &two;
167 }
168 while r <= c.mul_pow2(half.copy())
169 && r < large.mul_pow2(half.copy())
170 && ra < large.mul_pow2(half.copy())
171 && c > &small * &two
172 && ca > &small * &two
173 && f > &small * &two
174 {
175 c *= ½
176 r *= &two;
177 ca *= ½
178 ra *= &two;
179 f *= ½
180 }
181 if c.mul_pow2(half.copy()) + r.mul_pow2(half.copy())
182 >= &factor * &original
183 {
184 continue;
185 }
186 if (f < one() && scale[i] < &small / &f)
187 || (f > one() && scale[i] > &large / &f)
188 {
189 continue;
190 }
191 scale[i] *= &f;
192 let inv = f.recip();
193 for j in 0..n {
194 if j != i {
195 a[(i, j)] = a[(i, j)].mul_pow2(inv.copy());
196 a[(j, i)] = a[(j, i)].mul_pow2(f.copy());
197 }
198 }
199 changed = true;
200 }
201 if !changed {
202 break;
203 }
204 }
205 Ok(lo..hi)
206}
207
208/// Workspace for [`balance_eigenvectors_in_place`].
209///
210/// # Examples
211/// ```
212/// let _buffer = dyn_stack::MemBuffer::new(
213/// faer::linalg::evd::balance_eigenvectors_scratch::<f64>(3),
214/// );
215/// ```
216pub fn balance_eigenvectors_scratch<T: ComplexField>(dim: usize) -> StackReq {
217 temp_mat_scratch::<T>(dim, 1)
218}
219
220/// Back-transforms columns of right eigenvectors: `x_original = P D
221/// x_balanced`. Accepts any number of columns, including the real/imaginary
222/// pair encoding. The columns are not renormalized. `perm` and `scale` are the
223/// metadata from [`balance_in_place`].
224///
225/// # Panics
226/// Panics on incompatible lengths, a non-permutation, a nonpositive/nonfinite
227/// scale, or insufficient scratch. Metadata must describe the balanced input.
228///
229/// # Examples
230/// ```
231/// use dyn_stack::{MemBuffer, MemStack};
232/// use faer::Par;
233/// use faer::linalg::evd::*;
234/// let mut x = faer::mat![[1.0f64], [2.0]];
235/// let mut mem = MemBuffer::new(balance_eigenvectors_scratch::<f64>(2));
236/// balance_eigenvectors_in_place(
237/// x.as_mut(),
238/// &[1, 0],
239/// &[2.0, 0.5],
240/// Par::Seq,
241/// MemStack::new(&mut mem),
242/// );
243/// assert_eq!(x, faer::mat![[1.0], [2.0]]);
244/// ```
245pub fn balance_eigenvectors_in_place<T: ComplexField>(
246 mut x: MatMut<'_, T>,
247 perm: &[usize],
248 scale: &[T::Real],
249 _par: Par,
250 stack: &mut MemStack,
251) {
252 let n = x.nrows();
253 assert!(perm.len() == n && scale.len() == n);
254 let (mut tmp, _) = temp_mat_zeroed::<T, _, _>(n, 1, stack);
255 let mut tmp = tmp.as_mat_mut().col_mut(0);
256 // Use the output-sized workspace to validate the permutation once.
257 for &i in perm {
258 assert!(i < n && tmp[i] == zero());
259 tmp[i] = one();
260 }
261 assert!(scale.iter().all(|s| s.is_finite() && *s > zero()));
262 for j in 0..x.ncols() {
263 for i in 0..n {
264 tmp[perm[i]] = x[(i, j)].mul_pow2(scale[i].copy());
265 }
266 x.rb_mut().col_mut(j).copy_from(tmp.rb());
267 }
268}