Skip to main content
← OpenMECP Documentation

omecp/
hessian_update.rs

1//! Hessian update methods for optimization algorithms.
2//!
3//! This module implements various Hessian and inverse Hessian update formulas
4//!
5//! # Available Update Methods
6//!
7//! - **BFGS**: Broyden-Fletcher-Goldfarb-Shanno for minima
8//! - **Bofill**: Weighted Powell/Murtagh-Sargent for saddle points
9//! - **Powell**: Symmetric rank-one update
10//! - **PSB**: Powell-Symmetric-Broyden (legacy)
11//!
12//! # References
13//!
14//! - Bofill, J. M. J. Comput. Chem. 1994, 15, 1-11.
15//! - Powell, M. J. D. Math. Programming 1971, 1, 26-57.
16//! - Murtagh, B. A.; Sargent, R. W. H. Comput. J. 1970, 13, 185-194.
17
18use nalgebra::{DMatrix, DVector};
19
20/// Hessian update method selection.
21///
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum HessianUpdateMethod {
24    /// BFGS update for minima (MthUpd=3)
25    #[default]
26    Bfgs,
27    /// Bofill weighted update for saddle points (MthUpd=4)
28    Bofill,
29    /// Pure BFGS without curvature check (MthUpd=5)
30    BfgsPure,
31    /// Powell symmetric rank-one update (MthUpd=6)
32    Powell,
33    /// BFGS/Powell mixture following Bofill (MthUpd=7)
34    BfgsPowellMix,
35}
36
37
38/// Numerical thresholds for Hessian updates.
39const SMALL: f64 = 1e-14;
40const RMIN2: f64 = 1e-12;
41
42/// Updates the Hessian matrix using the specified method.
43///
44/// This is the main entry point for Hessian updates, dispatching to the
45/// appropriate algorithm based on the selected method.
46///
47/// # Arguments
48///
49/// * `hessian` - Current Hessian matrix (Ha/Ų)
50/// * `delta_x` - Step vector (x_new - x_old) in Angstrom
51/// * `delta_g` - Gradient difference (g_new - g_old) in Ha/Å
52/// * `method` - Update method to use
53///
54/// # Returns
55///
56/// Updated Hessian matrix in Ha/Ų.
57pub fn update_hessian_with_method(
58    hessian: &DMatrix<f64>,
59    delta_x: &DVector<f64>,
60    delta_g: &DVector<f64>,
61    method: HessianUpdateMethod,
62) -> DMatrix<f64> {
63    match method {
64        HessianUpdateMethod::Bfgs => update_hessian_bfgs(hessian, delta_x, delta_g),
65        HessianUpdateMethod::Bofill => update_hessian_bofill(hessian, delta_x, delta_g),
66        HessianUpdateMethod::BfgsPure => update_hessian_bfgs_pure(hessian, delta_x, delta_g),
67        HessianUpdateMethod::Powell => update_hessian_powell(hessian, delta_x, delta_g),
68        HessianUpdateMethod::BfgsPowellMix => update_hessian_bfgs_powell_mix(hessian, delta_x, delta_g),
69    }
70}
71
72/// BFGS Hessian update for minima (MthUpd=3).
73///
74/// Implements the standard BFGS formula from Old Code D2CorX:
75/// ```text
76/// H_new = H + (Δg·Δg^T)/(Δx·Δg) - (H·Δx·Δx^T·H)/(Δx^T·H·Δx)
77/// ```
78///
79/// # Curvature Condition
80///
81/// The update is only applied if Δx·Δg > 0 (positive curvature).
82/// This ensures the updated Hessian remains positive definite for minima.
83pub fn update_hessian_bfgs(
84    hessian: &DMatrix<f64>,
85    delta_x: &DVector<f64>,
86    delta_g: &DVector<f64>,
87) -> DMatrix<f64> {
88    let mut h_new = hessian.clone();
89    
90    // Check for valid inputs
91    if !delta_x.iter().all(|v| v.is_finite()) || !delta_g.iter().all(|v| v.is_finite()) {
92        return h_new;
93    }
94    
95    let dx_dg = delta_x.dot(delta_g);  // Δx·Δg
96    
97    // Curvature condition: skip if not positive
98    if dx_dg <= SMALL {
99        return h_new;
100    }
101    
102    // Compute H·Δx
103    let h_dx = hessian * delta_x;
104    let dx_h_dx = delta_x.dot(&h_dx);  // Δx^T·H·Δx
105    
106    if dx_h_dx.abs() <= SMALL {
107        return h_new;
108    }
109    
110    // BFGS update: H + (Δg·Δg^T)/(Δx·Δg) - (H·Δx)(H·Δx)^T/(Δx^T·H·Δx)
111    let n = hessian.nrows();
112    for i in 0..n {
113        for j in 0..=i {
114            let update = delta_g[i] * delta_g[j] / dx_dg 
115                       - h_dx[i] * h_dx[j] / dx_h_dx;
116            h_new[(i, j)] += update;
117            if i != j {
118                h_new[(j, i)] += update;
119            }
120        }
121    }
122    
123    h_new
124}
125
126/// Pure BFGS update without curvature check (MthUpd=5).
127///
128/// Same as BFGS but proceeds even with negative curvature.
129/// Use with caution - may produce indefinite Hessian.
130pub fn update_hessian_bfgs_pure(
131    hessian: &DMatrix<f64>,
132    delta_x: &DVector<f64>,
133    delta_g: &DVector<f64>,
134) -> DMatrix<f64> {
135    let mut h_new = hessian.clone();
136    
137    if !delta_x.iter().all(|v| v.is_finite()) || !delta_g.iter().all(|v| v.is_finite()) {
138        return h_new;
139    }
140    
141    let dx_dg = delta_x.dot(delta_g);
142    
143    if dx_dg.abs() <= SMALL {
144        return h_new;
145    }
146    
147    let h_dx = hessian * delta_x;
148    let dx_h_dx = delta_x.dot(&h_dx);
149    
150    if dx_h_dx.abs() <= SMALL {
151        return h_new;
152    }
153    
154    let n = hessian.nrows();
155    for i in 0..n {
156        for j in 0..=i {
157            let update = delta_g[i] * delta_g[j] / dx_dg 
158                       - h_dx[i] * h_dx[j] / dx_h_dx;
159            h_new[(i, j)] += update;
160            if i != j {
161                h_new[(j, i)] += update;
162            }
163        }
164    }
165    
166    h_new
167}
168
169/// Powell symmetric rank-one update.
170///
171/// Implements the Powell/SR1 formula:
172/// ```text
173/// H_new = H + (Δg - H·Δx)(Δg - H·Δx)^T / [(Δg - H·Δx)·Δx]
174/// ```
175///
176/// This update can handle negative curvature, making it suitable
177/// for transition state searches.
178pub fn update_hessian_powell(
179    hessian: &DMatrix<f64>,
180    delta_x: &DVector<f64>,
181    delta_g: &DVector<f64>,
182) -> DMatrix<f64> {
183    let mut h_new = hessian.clone();
184    
185    if !delta_x.iter().all(|v| v.is_finite()) || !delta_g.iter().all(|v| v.is_finite()) {
186        return h_new;
187    }
188    
189    let dx_norm_sq = delta_x.norm_squared();
190    if dx_norm_sq < RMIN2 {
191        return h_new;
192    }
193    
194    // Compute Δg - H·Δx
195    let h_dx = hessian * delta_x;
196    let diff = delta_g - &h_dx;
197    
198    // Denominator: (Δg - H·Δx)·Δx
199    let denom = diff.dot(delta_x);
200    
201    if denom.abs() <= SMALL {
202        return h_new;
203    }
204    
205    // Powell/SR1 update
206    let n = hessian.nrows();
207    for i in 0..n {
208        for j in 0..=i {
209            let update = diff[i] * diff[j] / denom;
210            h_new[(i, j)] += update;
211            if i != j {
212                h_new[(j, i)] += update;
213            }
214        }
215    }
216    
217    h_new
218}
219
220/// Bofill weighted update for saddle points.
221///
222/// Implements Bofill's formula from J. Comput. Chem. 1994, 15, 1-11:
223/// ```text
224/// H_new = H + φ·Powell_term + (1-φ)·MS_term
225/// ```
226///
227/// where:
228/// - φ = 1 - (Δx·Δg - Δx·H·Δx)² / (|Δx|² · |Δg - H·Δx|²)
229/// - Powell_term = symmetric rank-one update
230/// - MS_term = Murtagh-Sargent update
231///
232/// This weighted combination provides good convergence for both
233/// minima and saddle points.
234pub fn update_hessian_bofill(
235    hessian: &DMatrix<f64>,
236    delta_x: &DVector<f64>,
237    delta_g: &DVector<f64>,
238) -> DMatrix<f64> {
239    let mut h_new = hessian.clone();
240    
241    if !delta_x.iter().all(|v| v.is_finite()) || !delta_g.iter().all(|v| v.is_finite()) {
242        return h_new;
243    }
244    
245    let dx_norm_sq = delta_x.norm_squared();
246    if dx_norm_sq < RMIN2 {
247        return h_new;
248    }
249    
250    // Compute intermediate quantities
251    let h_dx = hessian * delta_x;
252    let diff = delta_g - &h_dx;  // Δg - H·Δx
253    let diff_norm_sq = diff.norm_squared();
254    
255    if diff_norm_sq < SMALL {
256        return h_new;
257    }
258    
259    let dx_dg = delta_x.dot(delta_g);
260    let dx_h_dx = delta_x.dot(&h_dx);
261    
262    // Compute Bofill weight φ
263    let r_num = dx_dg - dx_h_dx;
264    let r_denom = dx_norm_sq * diff_norm_sq;
265    
266    let phi = if r_num.abs() > SMALL && r_denom.abs() > SMALL {
267        1.0 - (r_num * r_num) / r_denom
268    } else {
269        1.0  // Default to pure Powell if denominators are small
270    };
271    
272    // Apply Bofill update: φ·Powell + (1-φ)·MS
273    let n = hessian.nrows();
274    for i in 0..n {
275        for j in 0..=i {
276            // Powell term: (Δg - H·Δx)_i · Δx_j + Δx_i · (Δg - H·Δx)_j
277            //            - (Δx·Δg - Δx·H·Δx) · Δx_i · Δx_j / |Δx|²
278            let powell = (diff[i] * delta_x[j] + delta_x[i] * diff[j]) / dx_norm_sq
279                       - r_num * delta_x[i] * delta_x[j] / (dx_norm_sq * dx_norm_sq);
280            
281            // Murtagh-Sargent term: (Δg - H·Δx)_i · (Δg - H·Δx)_j / (Δx·Δg - Δx·H·Δx)
282            let ms = if r_num.abs() > SMALL {
283                diff[i] * diff[j] / r_num
284            } else {
285                0.0
286            };
287            
288            let update = (1.0 - phi) * ms + phi * powell;
289            h_new[(i, j)] += update;
290            if i != j {
291                h_new[(j, i)] += update;
292            }
293        }
294    }
295    
296    h_new
297}
298
299/// BFGS/Powell mixture following Bofill weighting.
300///
301/// Uses Bofill's φ parameter to blend BFGS and Powell updates.
302/// Provides smooth transition between methods based on local curvature.
303pub fn update_hessian_bfgs_powell_mix(
304    hessian: &DMatrix<f64>,
305    delta_x: &DVector<f64>,
306    delta_g: &DVector<f64>,
307) -> DMatrix<f64> {
308    let mut h_new = hessian.clone();
309    
310    if !delta_x.iter().all(|v| v.is_finite()) || !delta_g.iter().all(|v| v.is_finite()) {
311        return h_new;
312    }
313    
314    let dx_norm_sq = delta_x.norm_squared();
315    if dx_norm_sq < RMIN2 {
316        return h_new;
317    }
318    
319    let h_dx = hessian * delta_x;
320    let diff = delta_g - &h_dx;
321    let diff_norm_sq = diff.norm_squared();
322    
323    if diff_norm_sq < SMALL {
324        return h_new;
325    }
326    
327    let dx_dg = delta_x.dot(delta_g);
328    let dx_h_dx = delta_x.dot(&h_dx);
329    
330    // Compute Bofill weight
331    let r_num = dx_dg - dx_h_dx;
332    let r_denom = dx_norm_sq * diff_norm_sq;
333    
334    let phi = if r_num.abs() > SMALL && r_denom.abs() > SMALL {
335        (1.0 - (r_num * r_num) / r_denom).clamp(0.0, 1.0)
336    } else {
337        0.5
338    };
339    
340    // Compute BFGS update terms
341    let bfgs_valid = dx_dg.abs() > SMALL && dx_h_dx.abs() > SMALL;
342    
343    // Compute Powell update terms  
344    let powell_denom = diff.dot(delta_x);
345    let powell_valid = powell_denom.abs() > SMALL;
346    
347    let n = hessian.nrows();
348    for i in 0..n {
349        for j in 0..=i {
350            let mut update = 0.0;
351            
352            // BFGS contribution (weighted by 1-φ)
353            if bfgs_valid {
354                let bfgs = delta_g[i] * delta_g[j] / dx_dg 
355                         - h_dx[i] * h_dx[j] / dx_h_dx;
356                update += (1.0 - phi) * bfgs;
357            }
358            
359            // Powell contribution (weighted by φ)
360            if powell_valid {
361                let powell = diff[i] * diff[j] / powell_denom;
362                update += phi * powell;
363            }
364            
365            h_new[(i, j)] += update;
366            if i != j {
367                h_new[(j, i)] += update;
368            }
369        }
370    }
371    
372    h_new
373}
374
375/// Updates the inverse Hessian using BFGS formula.
376///
377/// This is the standard BFGS inverse update used in the main optimizer:
378/// ```text
379/// H⁻¹_new = (I - ρ·s·y^T) · H⁻¹ · (I - ρ·y·s^T) + ρ·s·s^T
380/// ```
381/// where ρ = 1/(y^T·s), s = Δx, y = Δg.
382///
383/// Equivalent Old Code formula from UpdateX:
384/// ```text
385/// fac = 1 / (DelG · DelX)
386/// fad = 1 / (DelG · H_inv · DelG)
387/// w = fac * DelX - fad * H_inv · DelG
388/// H_inv_new = H_inv + fac * DelX * DelX^T - fad * HDelG * HDelG^T + fae * w * w^T
389/// ```
390pub fn update_inverse_hessian_bfgs(
391    h_inv: &DMatrix<f64>,
392    delta_x: &DVector<f64>,
393    delta_g: &DVector<f64>,
394) -> DMatrix<f64> {
395    if !delta_x.iter().all(|v| v.is_finite()) || !delta_g.iter().all(|v| v.is_finite()) {
396        return h_inv.clone();
397    }
398    if !h_inv.iter().all(|v| v.is_finite()) {
399        return h_inv.clone();
400    }
401
402    let mut h_inv_new = h_inv.clone();
403
404    // Old Code BFGS update for inverse Hessian
405    let h_del_g = h_inv * delta_g;
406    
407    let fac_denom = delta_g.dot(delta_x);  // DelG · DelX
408    let fae = delta_g.dot(&h_del_g);       // DelG · H_inv · DelG
409    
410    if fac_denom.abs() < SMALL || fae.abs() < SMALL {
411        return h_inv_new;
412    }
413    
414    let fac = 1.0 / fac_denom;
415    let fad = 1.0 / fae;
416    
417    // w = fac * DelX - fad * H_inv · DelG
418    let w = delta_x * fac - &h_del_g * fad;
419    
420    // H_inv_new = H_inv + fac * DelX * DelX^T - fad * HDelG * HDelG^T + fae * w * w^T
421    let term1 = (delta_x * delta_x.transpose()) * fac;
422    let term2 = (&h_del_g * h_del_g.transpose()) * fad;
423    let term3 = (&w * w.transpose()) * fae;
424    
425    h_inv_new += term1 - term2 + term3;
426
427    // Symmetrize
428    h_inv_new = 0.5 * (&h_inv_new + h_inv_new.transpose());
429    
430    // Clip non-finite entries
431    for v in h_inv_new.iter_mut() {
432        if !v.is_finite() {
433            *v = 0.0;
434        }
435    }
436
437    h_inv_new
438}