scirs2_sparse/cholesky_update/types.rs
1//! Types for sparse Cholesky modifications (rank-1 updates/downdates).
2//!
3//! This module defines the configuration, result, and classification types
4//! used by the Cholesky update and downdate algorithms.
5
6use std::fmt;
7
8/// Configuration for Cholesky update/downdate operations.
9///
10/// Controls numerical checks and tolerances used during the modification
11/// of a Cholesky factorization.
12///
13/// # Example
14///
15/// ```
16/// use scirs2_sparse::cholesky_update::CholUpdateConfig;
17///
18/// let config = CholUpdateConfig {
19/// check_positive_definite: true,
20/// tolerance: 1e-12,
21/// };
22/// ```
23#[derive(Debug, Clone, Copy)]
24pub struct CholUpdateConfig {
25 /// Whether to verify positive definiteness of the result.
26 ///
27 /// When `true`, the algorithm checks that all diagonal elements of the
28 /// updated factor remain positive, returning an error if the result
29 /// would be indefinite (particularly relevant for downdates).
30 pub check_positive_definite: bool,
31
32 /// Numerical tolerance for zero-checks and stability decisions.
33 ///
34 /// Diagonal elements below this threshold are considered numerically
35 /// zero, triggering a positive-definiteness failure when checking is
36 /// enabled.
37 pub tolerance: f64,
38}
39
40impl Default for CholUpdateConfig {
41 fn default() -> Self {
42 Self {
43 check_positive_definite: true,
44 tolerance: 1e-14,
45 }
46 }
47}
48
49/// Result of a Cholesky update or downdate operation.
50///
51/// Contains the modified Cholesky factor along with diagnostic information
52/// about the success and numerical conditioning of the operation.
53///
54/// # Representation
55///
56/// The factor is stored as a dense lower-triangular matrix in row-major
57/// order: `factor[i][j]` is the element at row `i`, column `j`, with
58/// `factor[i][j] == 0` for `j > i`.
59#[derive(Debug, Clone)]
60pub struct CholUpdateResult {
61 /// The modified lower-triangular Cholesky factor L'.
62 ///
63 /// Satisfies L' L'^T = A' where A' is the modified matrix.
64 pub factor: Vec<Vec<f64>>,
65
66 /// Whether the update completed successfully.
67 ///
68 /// For updates this is almost always `true`; for downdates it may be
69 /// `false` if the result would be indefinite (only when
70 /// `check_positive_definite` is enabled in the config).
71 pub successful: bool,
72
73 /// Rough estimate of the condition number of the modified factor.
74 ///
75 /// Computed as `max(diag) / min(diag)` of the factor, giving a cheap
76 /// (but often useful) lower bound on the true condition number.
77 pub condition_estimate: f64,
78}
79
80impl fmt::Display for CholUpdateResult {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(
83 f,
84 "CholUpdateResult {{ n={}, successful={}, cond_est={:.6e} }}",
85 self.factor.len(),
86 self.successful,
87 self.condition_estimate,
88 )
89 }
90}
91
92/// Classification of Cholesky modification types.
93///
94/// Describes the kind of rank-structured perturbation applied to the
95/// original matrix A = L L^T.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[non_exhaustive]
98pub enum UpdateType {
99 /// Rank-1 update: A' = A + α u u^T (α > 0).
100 RankOneUpdate,
101 /// Rank-1 downdate: A' = A − α u u^T (α > 0).
102 RankOneDowndate,
103 /// Low-rank update: A' = A + W D W^T where D is diagonal.
104 LowRankUpdate,
105}
106
107impl fmt::Display for UpdateType {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 UpdateType::RankOneUpdate => write!(f, "Rank-1 Update"),
111 UpdateType::RankOneDowndate => write!(f, "Rank-1 Downdate"),
112 UpdateType::LowRankUpdate => write!(f, "Low-Rank Update"),
113 #[allow(unreachable_patterns)]
114 _ => write!(f, "Unknown Update Type"),
115 }
116 }
117}
118
119/// Estimate the condition number of a lower-triangular matrix from its diagonal.
120///
121/// Returns `max(|diag|) / min(|diag|)`. If any diagonal element is zero
122/// (or below `tol`), returns `f64::INFINITY`.
123pub(crate) fn estimate_condition(l: &[Vec<f64>], tol: f64) -> f64 {
124 let n = l.len();
125 if n == 0 {
126 return 1.0;
127 }
128
129 let mut min_diag = f64::INFINITY;
130 let mut max_diag = 0.0_f64;
131
132 for i in 0..n {
133 let d = l[i][i].abs();
134 if d < tol {
135 return f64::INFINITY;
136 }
137 if d < min_diag {
138 min_diag = d;
139 }
140 if d > max_diag {
141 max_diag = d;
142 }
143 }
144
145 if min_diag <= 0.0 {
146 f64::INFINITY
147 } else {
148 max_diag / min_diag
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_default_config() {
158 let cfg = CholUpdateConfig::default();
159 assert!(cfg.check_positive_definite);
160 assert!(cfg.tolerance > 0.0);
161 assert!(cfg.tolerance < 1e-10);
162 }
163
164 #[test]
165 fn test_update_type_display() {
166 assert_eq!(format!("{}", UpdateType::RankOneUpdate), "Rank-1 Update");
167 assert_eq!(
168 format!("{}", UpdateType::RankOneDowndate),
169 "Rank-1 Downdate"
170 );
171 assert_eq!(format!("{}", UpdateType::LowRankUpdate), "Low-Rank Update");
172 }
173
174 #[test]
175 fn test_estimate_condition_identity() {
176 let l = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
177 let cond = estimate_condition(&l, 1e-14);
178 assert!((cond - 1.0).abs() < 1e-14);
179 }
180
181 #[test]
182 fn test_estimate_condition_singular() {
183 let l = vec![vec![1.0, 0.0], vec![0.5, 0.0]];
184 let cond = estimate_condition(&l, 1e-14);
185 assert!(cond.is_infinite());
186 }
187
188 #[test]
189 fn test_result_display() {
190 let res = CholUpdateResult {
191 factor: vec![vec![1.0]],
192 successful: true,
193 condition_estimate: 1.0,
194 };
195 let s = format!("{}", res);
196 assert!(s.contains("successful=true"));
197 }
198}