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