1use faer::linalg::solvers::Solve;
8use faer::sparse::{SparseColMat, Triplet};
9use faer::{Mat, Side};
10use nalgebra::{DMatrix, SVD as NalgebraSvd, SymmetricEigen};
11use num_complex::Complex64;
12
13use crate::{QmbedError, Result};
14
15pub(crate) struct HermitianEigensystem {
17 pub(crate) eigenvalues: Vec<f64>,
18 pub(crate) eigenvectors: Vec<Vec<Complex64>>,
20}
21
22pub(crate) struct ComplexEigensystem {
24 pub(crate) eigenvalues: Vec<Complex64>,
25 pub(crate) eigenvectors: Vec<Vec<Complex64>>,
27}
28
29fn validate_square_dense(values: &[Complex64], dimension: usize) -> Result<()> {
30 if values.len() != dimension.saturating_mul(dimension) {
31 return Err(QmbedError::DimensionMismatch(format!(
32 "dense backend expected {} values for a {dimension}x{dimension} matrix, got {}",
33 dimension.saturating_mul(dimension),
34 values.len()
35 )));
36 }
37 Ok(())
38}
39
40pub(crate) fn singular_values(
42 values: &[Complex64],
43 rows: usize,
44 columns: usize,
45) -> Result<Vec<f64>> {
46 if values.len() != rows.saturating_mul(columns) {
47 return Err(QmbedError::DimensionMismatch(format!(
48 "dense backend expected {} values for a {rows}x{columns} matrix, got {}",
49 rows.saturating_mul(columns),
50 values.len()
51 )));
52 }
53 let matrix =
54 Mat::<Complex64>::from_fn(rows, columns, |row, column| values[row * columns + column]);
55 if let Ok(decomposition) = matrix.thin_svd() {
56 let singular_values = (0..rows.min(columns))
57 .map(|index| decomposition.S()[index].re)
58 .collect::<Vec<_>>();
59 if singular_values.iter().all(|value| value.is_finite()) {
60 return Ok(singular_values);
61 }
62 }
63
64 let matrix = DMatrix::<Complex64>::from_row_slice(rows, columns, values);
65 let max_iterations = rows
66 .min(columns)
67 .saturating_mul(rows.max(columns))
68 .saturating_mul(64)
69 .max(1_024);
70 let decomposition =
71 NalgebraSvd::try_new(matrix, false, false, 64.0 * f64::EPSILON, max_iterations)
72 .ok_or_else(|| {
73 QmbedError::UnsupportedBackend(
74 "singular-value decomposition failed in both backends".into(),
75 )
76 })?;
77 let singular_values = decomposition
78 .singular_values
79 .iter()
80 .copied()
81 .collect::<Vec<_>>();
82 if singular_values.iter().any(|value| !value.is_finite()) {
83 return Err(QmbedError::UnsupportedBackend(
84 "singular-value decomposition produced non-finite values".into(),
85 ));
86 }
87 Ok(singular_values)
88}
89
90pub(crate) fn hermitian_eigenpairs(
92 values: &[Complex64],
93 dimension: usize,
94) -> Result<HermitianEigensystem> {
95 validate_square_dense(values, dimension)?;
96 let matrix = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
97 values[row * dimension + column]
98 });
99 match matrix.self_adjoint_eigen(Side::Lower) {
100 Ok(decomposition) => Ok(HermitianEigensystem {
101 eigenvalues: (0..dimension)
102 .map(|index| decomposition.S()[index].re)
103 .collect(),
104 eigenvectors: (0..dimension)
105 .map(|column| {
106 (0..dimension)
107 .map(|row| decomposition.U()[(row, column)])
108 .collect()
109 })
110 .collect(),
111 }),
112 Err(primary_error) => {
113 let matrix = DMatrix::<Complex64>::from_row_slice(dimension, dimension, values);
120 let max_iterations = dimension
121 .saturating_mul(dimension)
122 .saturating_mul(64)
123 .max(1_024);
124 let decomposition =
125 SymmetricEigen::try_new(matrix, 64.0 * f64::EPSILON, max_iterations).ok_or_else(
126 || {
127 QmbedError::UnsupportedBackend(format!(
128 "self-adjoint eigendecomposition failed in both backends; \
129 primary error: {primary_error:?}"
130 ))
131 },
132 )?;
133 let mut order: Vec<_> = (0..dimension).collect();
134 order.sort_by(|&left, &right| {
135 decomposition.eigenvalues[left]
136 .total_cmp(&decomposition.eigenvalues[right])
137 .then_with(|| left.cmp(&right))
138 });
139 Ok(HermitianEigensystem {
140 eigenvalues: order
141 .iter()
142 .map(|&index| decomposition.eigenvalues[index])
143 .collect(),
144 eigenvectors: order
145 .iter()
146 .map(|&column| {
147 (0..dimension)
148 .map(|row| decomposition.eigenvectors[(row, column)])
149 .collect()
150 })
151 .collect(),
152 })
153 }
154 }
155}
156
157pub(crate) fn complex_eigenpairs(
159 values: &[Complex64],
160 dimension: usize,
161) -> Result<ComplexEigensystem> {
162 validate_square_dense(values, dimension)?;
163 let matrix = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
164 values[row * dimension + column]
165 });
166 let decomposition = matrix.eigen().map_err(|error| {
167 QmbedError::UnsupportedBackend(format!("complex eigendecomposition failed: {error:?}"))
168 })?;
169 Ok(ComplexEigensystem {
170 eigenvalues: (0..dimension)
171 .map(|index| decomposition.S()[index])
172 .collect(),
173 eigenvectors: (0..dimension)
174 .map(|column| {
175 (0..dimension)
176 .map(|row| decomposition.U()[(row, column)])
177 .collect()
178 })
179 .collect(),
180 })
181}
182
183pub(crate) fn hermitian_exponential(
185 values: &[Complex64],
186 dimension: usize,
187 coefficient: Complex64,
188) -> Result<Vec<Complex64>> {
189 validate_square_dense(values, dimension)?;
190 if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
191 return Err(QmbedError::InvalidOptions(
192 "matrix exponential coefficient must be finite".into(),
193 ));
194 }
195 let eigensystem = hermitian_eigenpairs(values, dimension)?;
196 let vectors = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
197 eigensystem.eigenvectors[column][row]
198 });
199 let weighted = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
200 vectors[(row, column)] * (coefficient * eigensystem.eigenvalues[column]).exp()
201 });
202 let exponential = &weighted * vectors.adjoint();
203 let mut output = Vec::with_capacity(dimension.saturating_mul(dimension));
204 for row in 0..dimension {
205 for column in 0..dimension {
206 output.push(exponential[(row, column)]);
207 }
208 }
209 Ok(output)
210}
211
212pub(crate) fn square_matmul(
214 left: &[Complex64],
215 right: &[Complex64],
216 dimension: usize,
217) -> Result<Vec<Complex64>> {
218 validate_square_dense(left, dimension)?;
219 validate_square_dense(right, dimension)?;
220 let left = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
221 left[row * dimension + column]
222 });
223 let right = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
224 right[row * dimension + column]
225 });
226 let product = &left * &right;
227 let mut output = Vec::with_capacity(dimension.saturating_mul(dimension));
228 for row in 0..dimension {
229 for column in 0..dimension {
230 output.push(product[(row, column)]);
231 }
232 }
233 Ok(output)
234}
235
236pub(crate) fn unitarity_residual(values: &[Complex64], dimension: usize) -> Result<f64> {
238 validate_square_dense(values, dimension)?;
239 if dimension == 0 {
240 return Ok(0.0);
241 }
242 let matrix = Mat::<Complex64>::from_fn(dimension, dimension, |row, column| {
243 values[row * dimension + column]
244 });
245 let gram = matrix.adjoint() * &matrix;
246 let mut squared = 0.0;
247 for row in 0..dimension {
248 for column in 0..dimension {
249 let expected = if row == column {
250 Complex64::new(1.0, 0.0)
251 } else {
252 Complex64::new(0.0, 0.0)
253 };
254 squared += (gram[(row, column)] - expected).norm_sqr();
255 }
256 }
257 Ok(squared.sqrt() / dimension as f64)
258}
259
260pub trait ShiftedLinearSolver: Send + Sync {
262 fn solve(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()>;
263
264 fn supports_real(&self) -> bool {
265 false
266 }
267
268 fn solve_real(&self, _input: &[f64], _output: &mut [f64]) -> Result<()> {
269 Err(QmbedError::UnsupportedBackend(
270 "shifted factorization does not support real right-hand sides".into(),
271 ))
272 }
273}
274
275enum FaerShiftedFactorization {
276 Real(faer::sparse::linalg::solvers::Lu<usize, f64>),
277 Complex(faer::sparse::linalg::solvers::Lu<usize, Complex64>),
278}
279
280struct FaerShiftedSolver {
281 factorization: FaerShiftedFactorization,
282 dimension: usize,
283}
284
285impl ShiftedLinearSolver for FaerShiftedSolver {
286 fn solve(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
287 if input.len() != self.dimension || output.len() != self.dimension {
288 return Err(QmbedError::DimensionMismatch(
289 "shifted solve input or output length does not match".into(),
290 ));
291 }
292 match &self.factorization {
293 FaerShiftedFactorization::Real(factorization) => {
294 let mut real = faer::Col::from_fn(self.dimension, |index| input[index].re);
295 let mut imaginary = faer::Col::from_fn(self.dimension, |index| input[index].im);
296 factorization.solve_in_place(real.as_mut());
297 factorization.solve_in_place(imaginary.as_mut());
298 for (index, value) in output.iter_mut().enumerate() {
299 *value = Complex64::new(real[index], imaginary[index]);
300 }
301 }
302 FaerShiftedFactorization::Complex(factorization) => {
303 let mut right_hand_side = faer::Col::from_fn(self.dimension, |index| input[index]);
304 factorization.solve_in_place(right_hand_side.as_mut());
305 for (index, value) in output.iter_mut().enumerate() {
306 *value = right_hand_side[index];
307 }
308 }
309 }
310 Ok(())
311 }
312
313 fn supports_real(&self) -> bool {
314 matches!(self.factorization, FaerShiftedFactorization::Real(_))
315 }
316
317 fn solve_real(&self, input: &[f64], output: &mut [f64]) -> Result<()> {
318 if input.len() != self.dimension || output.len() != self.dimension {
319 return Err(QmbedError::DimensionMismatch(
320 "real shifted solve input or output length does not match".into(),
321 ));
322 }
323 let FaerShiftedFactorization::Real(factorization) = &self.factorization else {
324 return Err(QmbedError::UnsupportedBackend(
325 "complex shifted factorization cannot use the real fast path".into(),
326 ));
327 };
328 let mut right_hand_side = faer::Col::from_fn(self.dimension, |index| input[index]);
329 factorization.solve_in_place(right_hand_side.as_mut());
330 for (index, value) in output.iter_mut().enumerate() {
331 *value = right_hand_side[index];
332 }
333 Ok(())
334 }
335}
336
337pub(crate) fn factor_shifted_csc(
339 dimension: usize,
340 column_offsets: &[usize],
341 row_indices: &[usize],
342 values: &[Complex64],
343 shift: f64,
344 is_real: bool,
345) -> Result<Box<dyn ShiftedLinearSolver>> {
346 if column_offsets.len() != dimension + 1
347 || row_indices.len() != values.len()
348 || !shift.is_finite()
349 {
350 return Err(QmbedError::DimensionMismatch(
351 "invalid CSC storage for shifted factorization".into(),
352 ));
353 }
354
355 if is_real {
356 let mut triplets = Vec::with_capacity(values.len() + dimension);
357 append_shifted_triplets(
358 dimension,
359 column_offsets,
360 row_indices,
361 values,
362 shift,
363 |row, column, value| {
364 triplets.push(Triplet::new(row, column, value.re));
365 },
366 );
367 let matrix =
368 SparseColMat::<usize, f64>::try_new_from_triplets(dimension, dimension, &triplets)
369 .map_err(|error| {
370 QmbedError::UnsupportedBackend(format!(
371 "could not construct real sparse shifted matrix: {error}"
372 ))
373 })?;
374 let factorization = matrix.sp_lu().map_err(|_| QmbedError::NonConvergence {
375 iterations: 0,
376 residual: f64::INFINITY,
377 })?;
378 return Ok(Box::new(FaerShiftedSolver {
379 factorization: FaerShiftedFactorization::Real(factorization),
380 dimension,
381 }));
382 }
383
384 let mut triplets = Vec::with_capacity(values.len() + dimension);
385 append_shifted_triplets(
386 dimension,
387 column_offsets,
388 row_indices,
389 values,
390 shift,
391 |row, column, value| {
392 triplets.push(Triplet::new(row, column, value));
393 },
394 );
395 let matrix =
396 SparseColMat::<usize, Complex64>::try_new_from_triplets(dimension, dimension, &triplets)
397 .map_err(|error| {
398 QmbedError::UnsupportedBackend(format!(
399 "could not construct complex sparse shifted matrix: {error}"
400 ))
401 })?;
402 let factorization = matrix.sp_lu().map_err(|_| QmbedError::NonConvergence {
403 iterations: 0,
404 residual: f64::INFINITY,
405 })?;
406 Ok(Box::new(FaerShiftedSolver {
407 factorization: FaerShiftedFactorization::Complex(factorization),
408 dimension,
409 }))
410}
411
412fn append_shifted_triplets(
413 dimension: usize,
414 column_offsets: &[usize],
415 row_indices: &[usize],
416 values: &[Complex64],
417 shift: f64,
418 mut append: impl FnMut(usize, usize, Complex64),
419) {
420 for column in 0..dimension {
421 let mut has_diagonal = false;
422 for position in column_offsets[column]..column_offsets[column + 1] {
423 let row = row_indices[position];
424 let mut value = values[position];
425 if row == column {
426 value -= shift;
427 has_diagonal = true;
428 }
429 append(row, column, value);
430 }
431 if !has_diagonal {
432 append(column, column, Complex64::new(-shift, 0.0));
433 }
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn dense_backends_preserve_hermitian_and_general_eigenpairs() {
443 let hermitian = [
444 Complex64::new(1.0, 0.0),
445 Complex64::new(0.0, 1.0),
446 Complex64::new(0.0, -1.0),
447 Complex64::new(1.0, 0.0),
448 ];
449 let result = hermitian_eigenpairs(&hermitian, 2).unwrap();
450 assert!((result.eigenvalues[0] - 0.0).abs() < 1.0e-12);
451 assert!((result.eigenvalues[1] - 2.0).abs() < 1.0e-12);
452
453 let rotation = [
454 Complex64::new(0.0, 0.0),
455 Complex64::new(-1.0, 0.0),
456 Complex64::new(1.0, 0.0),
457 Complex64::new(0.0, 0.0),
458 ];
459 let result = complex_eigenpairs(&rotation, 2).unwrap();
460 assert!(
461 result
462 .eigenvalues
463 .iter()
464 .all(|value| (value.norm() - 1.0).abs() < 1.0e-12)
465 );
466
467 let dimension = 5;
468 let mut hermitian = vec![Complex64::new(0.0, 0.0); dimension * dimension];
469 for row in 0..dimension {
470 hermitian[row * dimension + row] = Complex64::new(row as f64 - 1.5, 0.0);
471 for column in 0..row {
472 let value = Complex64::new(
473 (row + column + 1) as f64 / 7.0,
474 (row as f64 - column as f64) / 11.0,
475 );
476 hermitian[row * dimension + column] = value;
477 hermitian[column * dimension + row] = value.conj();
478 }
479 }
480 let result = hermitian_eigenpairs(&hermitian, dimension).unwrap();
481 for (&eigenvalue, eigenvector) in result.eigenvalues.iter().zip(&result.eigenvectors) {
482 let residual = (0..dimension)
483 .map(|row| {
484 let applied = (0..dimension)
485 .map(|column| hermitian[row * dimension + column] * eigenvector[column])
486 .sum::<Complex64>();
487 (applied - eigenvalue * eigenvector[row]).norm_sqr()
488 })
489 .sum::<f64>()
490 .sqrt();
491 assert!(residual < 1.0e-12, "Hermitian residual was {residual}");
492 }
493 }
494
495 #[test]
496 fn real_shifted_factorization_accepts_real_and_complex_rhs() {
497 let solver = factor_shifted_csc(
498 2,
499 &[0, 2, 4],
500 &[0, 1, 0, 1],
501 &[
502 Complex64::new(2.0, 0.0),
503 Complex64::new(1.0, 0.0),
504 Complex64::new(1.0, 0.0),
505 Complex64::new(3.0, 0.0),
506 ],
507 0.0,
508 true,
509 )
510 .unwrap();
511 assert!(solver.supports_real());
512 let mut real = [0.0; 2];
513 solver.solve_real(&[1.0, 0.0], &mut real).unwrap();
514 assert!((real[0] - 0.6).abs() < 1.0e-12);
515 assert!((real[1] + 0.2).abs() < 1.0e-12);
516
517 let mut complex = [Complex64::new(0.0, 0.0); 2];
518 solver
519 .solve(
520 &[Complex64::new(1.0, 2.0), Complex64::new(0.0, 0.0)],
521 &mut complex,
522 )
523 .unwrap();
524 assert!((complex[0] - Complex64::new(0.6, 1.2)).norm() < 1.0e-12);
525 assert!((complex[1] - Complex64::new(-0.2, -0.4)).norm() < 1.0e-12);
526 }
527}