use faer::{
Mat,
linalg::solvers::Solve,
sparse::SparseColMat,
sparse::linalg::solvers::{Qr, SymbolicQr},
};
use crate::error::ErrorLogging;
use crate::linalg::sparse::normal_eq::{LazyNormalEquations, NormalEquations};
use crate::linalg::{Damping, LinAlgError, LinAlgResult, LinearSolver, SparseMode};
#[derive(Debug, Clone)]
pub struct SparseQRSolver {
symbolic_factorization: Option<SymbolicQr<usize>>,
ne_cache: LazyNormalEquations,
hessian: Option<SparseColMat<usize, f64>>,
gradient: Option<Mat<f64>>,
}
impl SparseQRSolver {
pub fn new() -> Self {
SparseQRSolver {
symbolic_factorization: None,
ne_cache: LazyNormalEquations::default(),
hessian: None,
gradient: None,
}
}
pub fn hessian(&self) -> Option<&SparseColMat<usize, f64>> {
self.hessian.as_ref()
}
pub fn gradient(&self) -> Option<&Mat<f64>> {
self.gradient.as_ref()
}
}
impl Default for SparseQRSolver {
fn default() -> Self {
Self::new()
}
}
impl LinearSolver<SparseMode> for SparseQRSolver {
fn solve_normal_equation(
&mut self,
residuals: &Mat<f64>,
jacobians: &SparseColMat<usize, f64>,
) -> LinAlgResult<Mat<f64>> {
let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobians)?;
let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
cached_sym.clone()
} else {
let new_sym = SymbolicQr::try_new(hessian.symbolic()).map_err(|e| {
LinAlgError::FactorizationFailed("Symbolic QR decomposition failed".to_string())
.log_with_source(e)
})?;
self.symbolic_factorization = Some(new_sym.clone());
new_sym
};
let qr = Qr::try_new_with_symbolic(sym, hessian.as_ref()).map_err(|e| {
LinAlgError::SingularMatrix(
"QR factorization failed (matrix may be singular)".to_string(),
)
.log_with_source(e)
})?;
let dx = qr.solve(-&gradient);
self.hessian = Some(hessian);
self.gradient = Some(gradient);
Ok(dx)
}
fn solve_augmented_equation(
&mut self,
residuals: &Mat<f64>,
jacobians: &SparseColMat<usize, f64>,
damping: &Damping,
) -> LinAlgResult<Mat<f64>> {
let NormalEquations { hessian, gradient } = self.ne_cache.compute(residuals, jacobians)?;
let augmented_hessian = self.ne_cache.damped_hessian(damping)?;
let sym = if let Some(ref cached_sym) = self.symbolic_factorization {
cached_sym.clone()
} else {
let new_sym = SymbolicQr::try_new(augmented_hessian.symbolic()).map_err(|e| {
LinAlgError::FactorizationFailed(
"Symbolic QR decomposition failed for augmented system".to_string(),
)
.log_with_source(e)
})?;
self.symbolic_factorization = Some(new_sym.clone());
new_sym
};
let qr = Qr::try_new_with_symbolic(sym, augmented_hessian.as_ref()).map_err(|e| {
LinAlgError::SingularMatrix(
"QR factorization failed (matrix may be singular)".to_string(),
)
.log_with_source(e)
})?;
let dx = qr.solve(-&gradient);
self.hessian = Some(hessian);
self.gradient = Some(gradient);
Ok(dx)
}
fn hessian_vec_product(&self, v: &Mat<f64>) -> Option<Mat<f64>> {
Some(
<SparseMode as crate::linearizer::AssemblyBackend>::hessian_vec_product(
self.hessian.as_ref()?,
v,
),
)
}
fn get_hessian(&self) -> Option<&SparseColMat<usize, f64>> {
self.hessian.as_ref()
}
fn get_gradient(&self) -> Option<&Mat<f64>> {
self.gradient.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use faer::sparse::Triplet;
const TOLERANCE: f64 = 1e-10;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn create_test_data()
-> Result<(SparseColMat<usize, f64>, Mat<f64>), faer::sparse::CreationError> {
let triplets = vec![
Triplet::new(0, 0, 1.0),
Triplet::new(0, 1, 0.0),
Triplet::new(0, 2, 1.0),
Triplet::new(1, 0, 0.0),
Triplet::new(1, 1, 1.0),
Triplet::new(1, 2, 1.0),
Triplet::new(2, 0, 1.0),
Triplet::new(2, 1, 1.0),
Triplet::new(2, 2, 0.0),
Triplet::new(3, 0, 1.0),
Triplet::new(3, 1, 0.0),
Triplet::new(3, 2, 0.0),
];
let jacobian = SparseColMat::try_new_from_triplets(4, 3, &triplets)?;
let residuals = Mat::from_fn(4, 1, |i, _| (i + 1) as f64);
Ok((jacobian, residuals))
}
#[test]
fn test_qr_solver_creation() {
let solver = SparseQRSolver::new();
assert!(solver.hessian.is_none());
assert!(solver.gradient.is_none());
let default_solver = SparseQRSolver::default();
assert!(default_solver.hessian.is_none());
assert!(default_solver.gradient.is_none());
}
#[test]
fn test_qr_solve_normal_equation() -> TestResult {
let mut solver = SparseQRSolver::new();
let (jacobian, residuals) = create_test_data()?;
let solution =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert_eq!(solution.nrows(), 3); assert_eq!(solution.ncols(), 1);
Ok(())
}
#[test]
fn test_qr_factorizer_caching() -> TestResult {
let mut solver = SparseQRSolver::new();
let (jacobian, residuals) = create_test_data()?;
let sol1 =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
let sol2 =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
for i in 0..sol1.nrows() {
assert!((sol1[(i, 0)] - sol2[(i, 0)]).abs() < TOLERANCE);
}
Ok(())
}
#[test]
fn test_qr_solve_augmented_equation() -> TestResult {
let mut solver = SparseQRSolver::new();
let (jacobian, residuals) = create_test_data()?;
let lambda = 0.1;
let solution = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut solver,
&residuals,
&jacobian,
&Damping::identity(lambda),
)?;
assert_eq!(solution.nrows(), 3); assert_eq!(solution.ncols(), 1);
Ok(())
}
#[test]
fn test_qr_augmented_different_lambdas() -> TestResult {
let mut solver = SparseQRSolver::new();
let (jacobian, residuals) = create_test_data()?;
let lambda1 = 0.01;
let lambda2 = 1.0;
let sol1 = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut solver,
&residuals,
&jacobian,
&Damping::identity(lambda1),
)?;
let sol2 = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut solver,
&residuals,
&jacobian,
&Damping::identity(lambda2),
)?;
let mut different = false;
for i in 0..sol1.nrows() {
if (sol1[(i, 0)] - sol2[(i, 0)]).abs() > TOLERANCE {
different = true;
break;
}
}
assert!(
different,
"Solutions should differ with different lambda values"
);
Ok(())
}
#[test]
fn test_qr_rank_deficient_matrix() -> TestResult {
let mut solver = SparseQRSolver::new();
let triplets = vec![
Triplet::new(0, 0, 1.0),
Triplet::new(0, 1, 2.0),
Triplet::new(0, 2, 3.0),
Triplet::new(1, 0, 2.0),
Triplet::new(1, 1, 4.0),
Triplet::new(1, 2, 6.0), Triplet::new(2, 0, 0.0),
Triplet::new(2, 1, 0.0),
Triplet::new(2, 2, 1.0),
];
let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
let residuals = Mat::from_fn(3, 1, |i, _| i as f64);
let result =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian);
assert!(result.is_ok());
Ok(())
}
#[test]
fn test_qr_augmented_system_structure() -> TestResult {
let mut solver = SparseQRSolver::new();
let triplets = vec![
Triplet::new(0, 0, 1.0),
Triplet::new(0, 1, 0.0),
Triplet::new(1, 0, 0.0),
Triplet::new(1, 1, 1.0),
];
let jacobian = SparseColMat::try_new_from_triplets(2, 2, &triplets)?;
let residuals = Mat::from_fn(2, 1, |i, _| (i + 1) as f64);
let lambda = 0.5;
let solution = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut solver,
&residuals,
&jacobian,
&Damping::identity(lambda),
)?;
assert_eq!(solution.nrows(), 2); assert_eq!(solution.ncols(), 1);
Ok(())
}
#[test]
fn test_qr_numerical_accuracy() -> TestResult {
let mut solver = SparseQRSolver::new();
let triplets = vec![
Triplet::new(0, 0, 1.0),
Triplet::new(1, 1, 1.0),
Triplet::new(2, 2, 1.0),
];
let jacobian = SparseColMat::try_new_from_triplets(3, 3, &triplets)?;
let residuals = Mat::from_fn(3, 1, |i, _| -((i + 1) as f64));
let solution =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
for i in 0..3 {
let expected = (i + 1) as f64;
assert!(
(solution[(i, 0)] - expected).abs() < TOLERANCE,
"Expected {}, got {}",
expected,
solution[(i, 0)]
);
}
Ok(())
}
#[test]
fn test_qr_solver_clone() {
let solver1 = SparseQRSolver::new();
let solver2 = solver1.clone();
assert!(solver2.hessian.is_none());
assert!(solver2.gradient.is_none());
}
#[test]
fn test_qr_zero_lambda_augmented() -> TestResult {
let mut solver = SparseQRSolver::new();
let (jacobian, residuals) = create_test_data()?;
let normal_sol =
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
let augmented_sol = LinearSolver::<SparseMode>::solve_augmented_equation(
&mut solver,
&residuals,
&jacobian,
&Damping::identity(0.0),
)?;
for i in 0..normal_sol.nrows() {
assert!(
(normal_sol[(i, 0)] - augmented_sol[(i, 0)]).abs() < 1e-8,
"Zero lambda augmented should match normal equation"
);
}
Ok(())
}
#[test]
fn test_qr_hessian_getter() -> TestResult {
let mut solver = SparseQRSolver::new();
assert!(solver.hessian().is_none());
let (jacobian, residuals) = create_test_data()?;
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert!(solver.hessian().is_some());
Ok(())
}
#[test]
fn test_qr_gradient_getter() -> TestResult {
let mut solver = SparseQRSolver::new();
assert!(solver.gradient().is_none());
let (jacobian, residuals) = create_test_data()?;
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert!(solver.gradient().is_some());
Ok(())
}
#[test]
fn test_qr_get_hessian_trait() -> TestResult {
let mut solver = SparseQRSolver::new();
assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_none());
let (jacobian, residuals) = create_test_data()?;
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert!(LinearSolver::<SparseMode>::get_hessian(&solver).is_some());
Ok(())
}
#[test]
fn test_qr_get_gradient_trait() -> TestResult {
let mut solver = SparseQRSolver::new();
assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_none());
let (jacobian, residuals) = create_test_data()?;
LinearSolver::<SparseMode>::solve_normal_equation(&mut solver, &residuals, &jacobian)?;
assert!(LinearSolver::<SparseMode>::get_gradient(&solver).is_some());
Ok(())
}
}