@@ -68,7 +68,8 @@ pub fn matrix_square_root(matrix: &DMatrix<f64>) -> DMatrix<f64> {
6868///
6969/// # Returns
7070/// A symmetrized version of the input matrix.
71- fn symmetrize ( m : & DMatrix < f64 > ) -> DMatrix < f64 > {
71+ #[ inline]
72+ pub fn symmetrize ( m : & DMatrix < f64 > ) -> DMatrix < f64 > {
7273 0.5 * ( m + m. transpose ( ) )
7374}
7475/// Plain Cholesky square root
@@ -77,7 +78,7 @@ fn symmetrize(m: &DMatrix<f64>) -> DMatrix<f64> {
7778/// This is a quick way to initially attempt to calculate a matrix square root.
7879///
7980/// # Arguments
80- /// * `p` - the matrix to factor
81+ /// * `` p` - the matrix to factor
8182///
8283/// # Returns
8384/// A lower triangular matrix L such that P ≈ L Lᵀ, or None if it fails.
@@ -131,6 +132,64 @@ fn evd_symmetric_sqrt_with_floor(p: &DMatrix<f64>, floor: f64) -> DMatrix<f64> {
131132
132133}
133134
135+ #[ derive( Debug , Clone , Copy ) ]
136+ pub struct SolveOptions {
137+ pub initial_jitter : f64 , // e.g., 1e-12
138+ pub max_jitter : f64 , // e.g., 1e-6
139+ pub max_tries : usize , // e.g., 6
140+ }
141+
142+ impl Default for SolveOptions {
143+ fn default ( ) -> Self {
144+ Self { initial_jitter : 1e-12 , max_jitter : 1e-6 , max_tries : 6 }
145+ }
146+ }
147+ /// Solve A X = B for SPD-ish A via Cholesky, with jitter retries.
148+ /// Returns None if all attempts fail.
149+ pub fn chol_solve_spd (
150+ a : & DMatrix < f64 > ,
151+ b : & DMatrix < f64 > ,
152+ opt : SolveOptions ,
153+ ) -> Option < DMatrix < f64 > > {
154+ assert ! ( a. is_square( ) , "chol_solve_spd: A must be square" ) ;
155+ assert_eq ! ( a. nrows( ) , b. nrows( ) , "chol_solve_spd: A and B incompatible" ) ;
156+
157+ // Symmetrize first (SPD drift is common).
158+ let a_sym = symmetrize ( a) ;
159+
160+ // Try plain Cholesky
161+ if let Some ( ch) = Cholesky :: new ( a_sym. clone ( ) ) {
162+ return Some ( ch. solve ( b) ) ;
163+ }
164+
165+ // Jitter ramp
166+ let n = a_sym. nrows ( ) ;
167+ let mut jitter = opt. initial_jitter ;
168+ for _ in 0 ..opt. max_tries {
169+ let mut a_j = a_sym. clone ( ) ;
170+ for i in 0 ..n { a_j[ ( i, i) ] += jitter; }
171+ if let Some ( ch) = Cholesky :: new ( a_j) {
172+ return Some ( ch. solve ( b) ) ;
173+ }
174+ jitter *= 10.0 ;
175+ if jitter > opt. max_jitter { break ; }
176+ }
177+ None
178+ }
179+
180+ /// Robust SPD solve with sane defaults:
181+ /// - Cholesky + jitter (preferred)
182+ /// - Last resort: explicit inverse
183+ pub fn robust_spd_solve ( a : & DMatrix < f64 > , b : & DMatrix < f64 > ) -> DMatrix < f64 > {
184+ if let Some ( x) = chol_solve_spd ( a, b, SolveOptions :: default ( ) ) {
185+ x
186+ } else if let Some ( inv) = symmetrize ( a) . try_inverse ( ) {
187+ & inv * b
188+ } else {
189+ panic ! ( "robust_spd_solve: A is not invertible (even after jitter)." ) ;
190+ }
191+ }
192+
134193/* =============================== Tests ==================================== */
135194
136195#[ cfg( test) ]
0 commit comments