Skip to main content
← OpenMECP Documentation

omecp/
gdiis.rs

1//! GDIIS (Geometry Direct Inversion in Iterative Subspace) implementation.
2//!
3//! This module implements the experimental GDIIS algorithm
4//!
5//! # Algorithm Overview
6//!
7//! GDIIS accelerates geometry optimization by constructing an optimal linear
8//! combination of previous geometries and error vectors. The method:
9//!
10//! 1. Builds an overlap matrix A from error vectors: A[i,j] = <e_i, e_j>
11//! 2. Maintains A⁻¹ using SR1 (Symmetric Rank-One) updates
12//! 3. Solves for coefficients c such that Σc_i = 1 and residual is minimized
13//! 4. Validates solution using cosine and coefficient checks
14//!
15
16use nalgebra::{DMatrix, DVector};
17use std::collections::VecDeque;
18
19/// Cosine check mode for GDIIS validation.
20///
21/// Controls how the GDIIS step is validated against the last error vector.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum CosineCheckMode {
24    /// No cosine check (ICos=0)
25    None,
26    /// CosLim = 0.0 (ICos=1)
27    Zero,
28    /// CosLim = 0.71 (ICos=2)
29    #[default]
30    Standard,
31    /// Variable CosLim based on number of vectors used (ICos=3)
32    Variable,
33    /// CosLim = √3/2 ≈ 0.866 (ICos≥4)
34    Strict,
35}
36
37/// Coefficient check mode for GDIIS validation.
38///
39/// Controls validation of DIIS coefficients.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum CoeffCheckMode {
42    /// No coefficient check (IChkC=0)
43    None,
44    /// Regular coefficient check (IChkC=1)
45    #[default]
46    Regular,
47    /// Force last two vectors to have larger weight (IChkC=2)
48    ForceRecent,
49    /// Combined: Regular + ForceRecent (IChkC=3)
50    Combined,
51    /// Regular without cosine modification (IChkC=4)
52    RegularNoCosine,
53}
54
55/// Error types for GDIIS operations.
56#[derive(Debug, Clone)]
57pub enum GdiisError {
58    /// Matrix is singular or nearly singular
59    SingularMatrix,
60    /// Coefficient check failed
61    CoefficientCheckFailed {
62        /// Description of why the coefficient check failed
63        reason: String,
64    },
65    /// Cosine check failed - GDIIS step direction differs too much from last error vector
66    CosineCheckFailed {
67        /// Computed cosine between GDIIS step and last error vector
68        cos: f64,
69        /// Required minimum cosine limit
70        limit: f64,
71    },
72    /// Redundant vectors detected
73    RedundantVectors,
74    /// Error vector ratio too large
75    RatioTooLarge,
76    /// Insufficient history
77    InsufficientHistory,
78}
79
80
81/// Lower triangular matrix storage for DIIS overlap matrix.
82///
83/// Stores elements in packed format: [A(1,1), A(2,1), A(2,2), A(3,1), ...]
84#[derive(Debug, Clone)]
85pub struct TriangularMatrix {
86    /// Packed storage for lower triangular elements
87    data: Vec<f64>,
88    /// Matrix dimension
89    size: usize,
90}
91
92impl TriangularMatrix {
93    /// Creates a new triangular matrix of given size.
94    pub fn new(size: usize) -> Self {
95        let len = (size * (size + 1)) / 2;
96        Self {
97            data: vec![0.0; len],
98            size,
99        }
100    }
101
102    /// Returns the matrix dimension.
103    pub fn size(&self) -> usize {
104        self.size
105    }
106
107    /// Converts (i, j) indices to packed storage index.
108    #[inline]
109    fn index(&self, i: usize, j: usize) -> usize {
110        let (row, col) = if i >= j { (i, j) } else { (j, i) };
111        (row * (row + 1)) / 2 + col
112    }
113
114    /// Gets element at (i, j).
115    pub fn get(&self, i: usize, j: usize) -> f64 {
116        self.data[self.index(i, j)]
117    }
118
119    /// Sets element at (i, j).
120    pub fn set(&mut self, i: usize, j: usize, value: f64) {
121        let idx = self.index(i, j);
122        self.data[idx] = value;
123    }
124
125    /// Multiplies matrix by vector: y = A * x
126    ///
127    /// # Panics
128    ///
129    /// Panics if vector length doesn't match matrix size.
130    pub fn multiply_vector(&self, x: &[f64]) -> Vec<f64> {
131        let n = self.size;
132        // Use the minimum of matrix size and vector length to avoid out-of-bounds
133        let len = n.min(x.len());
134        let mut y = vec![0.0; len];
135        
136        for i in 0..len {
137            for j in 0..len {
138                y[i] += self.get(i, j) * x[j];
139            }
140        }
141        y
142    }
143
144    /// Scales all elements by a factor.
145    pub fn scale(&mut self, factor: f64) {
146        for v in &mut self.data {
147            *v *= factor;
148        }
149    }
150
151    /// Copies elements from another triangular matrix.
152    pub fn copy_from(&mut self, other: &TriangularMatrix) {
153        self.data.copy_from_slice(&other.data);
154    }
155
156    /// Extends matrix by one row/column, copying existing elements.
157    pub fn extend(&mut self) {
158        self.size += 1;
159        let new_len = (self.size * (self.size + 1)) / 2;
160        self.data.resize(new_len, 0.0);
161    }
162}
163
164/// DIIS coefficient solver using SR1 inverse updates.
165///
166/// Implements the DIISC algorithm.
167/// Solves the DIIS equations iteratively while maintaining A⁻¹.
168pub struct DiisCoeffSolver {
169    /// Convergence tolerance
170    convergence: f64,
171    /// Maximum coefficient sum allowed
172    max_coeff_sum: f64,
173}
174
175impl Default for DiisCoeffSolver {
176    fn default() -> Self {
177        Self {
178            convergence: 1e-8,
179            max_coeff_sum: 1e8,
180        }
181    }
182}
183
184impl DiisCoeffSolver {
185    /// Creates a new solver with specified convergence tolerance.
186    pub fn new(convergence: f64) -> Self {
187        Self {
188            convergence,
189            max_coeff_sum: 1e8,
190        }
191    }
192
193    /// Solves for DIIS coefficients using iterative SR1 updates.
194    ///
195    /// # Arguments
196    ///
197    /// * `a` - Lower triangular overlap matrix A[i,j] = <e_i, e_j>
198    /// * `a_inv` - Inverse of A (maintained via SR1 updates)
199    /// * `coeffs` - Initial coefficient guess (modified in place)
200    /// * `n_start` - Starting number of vectors (with existing solution)
201    /// * `n_total` - Total number of vectors to solve for
202    ///
203    /// # Returns
204    ///
205    /// Number of vectors actually used, or error if failed.
206    pub fn solve(
207        &self,
208        a: &TriangularMatrix,
209        a_inv: &mut TriangularMatrix,
210        coeffs: &mut [f64],
211        n_start: usize,
212        n_total: usize,
213    ) -> Result<usize, GdiisError> {
214        let mut n_used = n_start;
215
216        for n in (n_start + 1)..=n_total {
217            // Save previous coefficients
218            let coeffs_save: Vec<f64> = coeffs[..n].to_vec();
219
220            // Compute residual: y = 1 - A*c
221            let ac = a.multiply_vector(&coeffs[..n]);
222            let mut y: Vec<f64> = vec![1.0; n];
223            for i in 0..n {
224                y[i] -= ac[i];
225            }
226
227            let err_max = y.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
228            let _y_len = (y.iter().map(|v| v * v).sum::<f64>() / n as f64).sqrt();
229
230            let mut iter = 0;
231            let max_iter = 10 * n;
232
233            while err_max > self.convergence && iter <= max_iter {
234                // Compute dx = A_inv * y
235                let dx = a_inv.multiply_vector(&y);
236                
237                // Compute dy = A * dx
238                let dy = a.multiply_vector(&dx);
239
240                // SR1 update for A_inv
241                self.sr1_update(a_inv, &dy, &dx, n)?;
242
243                // Compute step scaling
244                let s11: f64 = dy.iter().map(|v| v * v).sum();
245                let s22: f64 = y.iter().map(|v| v * v).sum();
246                let s12: f64 = dy.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
247
248                let c12 = if s11 * s22 > 1e-30 {
249                    s12 / (s11 * s22).sqrt()
250                } else {
251                    0.0
252                };
253
254                let ss = if c12.abs() > 1e-3 && s11 > 1e-30 {
255                    c12 * (s22 / s11).sqrt()
256                } else {
257                    // Fallback: use y directly as search direction
258                    let dy_new = a.multiply_vector(&y);
259                    let s11_new: f64 = dy_new.iter().map(|v| v * v).sum();
260                    let s12_new: f64 = dy_new.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
261                    if s11_new > 1e-30 {
262                        s12_new / s11_new * (s22 / s11_new).sqrt()
263                    } else {
264                        1.0
265                    }
266                };
267
268                // Update coefficients
269                for i in 0..n {
270                    coeffs[i] += ss * dx[i];
271                }
272
273                // Check for divergence
274                let coeff_sum: f64 = coeffs[..n].iter().sum();
275                if coeff_sum.abs() > self.max_coeff_sum {
276                    // Revert to previous solution
277                    coeffs[..n].copy_from_slice(&coeffs_save);
278                    return Ok(n.saturating_sub(1).max(n_start));
279                }
280
281                // Check redundancy
282                let dx_len = (dx.iter().map(|v| v * v).sum::<f64>() / n as f64).sqrt();
283                if dx_len <= self.convergence * self.convergence {
284                    break;
285                }
286
287                // Recompute residual
288                let ac = a.multiply_vector(&coeffs[..n]);
289                for i in 0..n {
290                    y[i] = 1.0 - ac[i];
291                }
292
293                iter += 1;
294            }
295
296            // Check if DIIS failed due to redundancy
297            if (iter == 0 || iter > max_iter) && err_max > self.convergence {
298                coeffs[..n].copy_from_slice(&coeffs_save);
299                return Ok(n.saturating_sub(1).max(n_start));
300            }
301
302            n_used = n;
303        }
304
305        // Normalize coefficients
306        // Sc = One/Abs(CSum); Call AScale(NN,SC,C,C)
307        // This preserves the sign of coefficients while ensuring sum = ±1
308        let coeff_sum: f64 = coeffs[..n_used].iter().sum();
309        if coeff_sum.abs() <= 1e-4 {
310            // Sum too small - reset to use only last point 
311            for c in &mut coeffs[..n_used] {
312                *c = 0.0;
313            }
314            coeffs[0] = 1.0;
315        } else {
316            // Scale by 1/|sum| to normalize
317            let scale = 1.0 / coeff_sum.abs();
318            for c in &mut coeffs[..n_used] {
319                *c *= scale;
320            }
321        }
322
323        Ok(n_used)
324    }
325
326    /// SR1 update for inverse matrix.
327    fn sr1_update(
328        &self,
329        a_inv: &mut TriangularMatrix,
330        dy: &[f64],
331        dx: &[f64],
332        n: usize,
333    ) -> Result<(), GdiisError> {
334        // Compute diff = dx - A_inv * dy
335        let a_inv_dy = a_inv.multiply_vector(dy);
336        let mut diff = vec![0.0; n];
337        for i in 0..n {
338            diff[i] = dx[i] - a_inv_dy[i];
339        }
340
341        // Compute denominator: dy · diff
342        let denom: f64 = dy.iter().zip(diff.iter()).map(|(a, b)| a * b).sum();
343
344        if denom.abs() < 1e-14 {
345            return Ok(()); // Skip update if denominator too small
346        }
347
348        // SR1 update: A_inv += diff * diff^T / denom
349        for i in 0..n {
350            for j in 0..=i {
351                let update = diff[i] * diff[j] / denom;
352                let current = a_inv.get(i, j);
353                a_inv.set(i, j, current + update);
354            }
355        }
356
357        Ok(())
358    }
359}
360
361/// Main GDIIS optimizer.
362///
363/// Implements the full GDIIS algorithm with validation checks.
364pub struct GdiisOptimizer {
365    /// Maximum number of vectors to store
366    pub max_vectors: usize,
367    /// Cosine check mode
368    pub cosine_check: CosineCheckMode,
369    /// Coefficient check mode
370    pub coeff_check: CoeffCheckMode,
371    /// Overlap matrix A
372    a_matrix: TriangularMatrix,
373    /// Inverse of A
374    a_inv: TriangularMatrix,
375    /// Current scale factor
376    scale: f64,
377    /// Minimum error norm squared
378    el2_min: f64,
379    /// DIIS coefficient solver
380    solver: DiisCoeffSolver,
381}
382
383impl GdiisOptimizer {
384    /// Creates a new GDIIS optimizer.
385    pub fn new(max_vectors: usize) -> Self {
386        Self {
387            max_vectors,
388            cosine_check: CosineCheckMode::Standard,
389            coeff_check: CoeffCheckMode::Regular,
390            a_matrix: TriangularMatrix::new(0),
391            a_inv: TriangularMatrix::new(0),
392            scale: 1.0,
393            el2_min: 1.0,
394            solver: DiisCoeffSolver::default(),
395        }
396    }
397
398    /// Resets the optimizer state.
399    pub fn reset(&mut self) {
400        self.a_matrix = TriangularMatrix::new(0);
401        self.a_inv = TriangularMatrix::new(0);
402        self.scale = 1.0;
403        self.el2_min = 1.0;
404    }
405
406    /// Computes a GDIIS step.
407    ///
408    /// # Arguments
409    ///
410    /// * `coords` - History of coordinate vectors
411    /// * `errors` - History of error vectors (gradients or Newton steps)
412    /// * `hessians` - History of Hessian matrices (for error vector computation)
413    ///
414    /// # Returns
415    ///
416    /// Tuple of (new_coords, coefficients, n_used) or error.
417    pub fn compute_step(
418        &mut self,
419        coords: &VecDeque<DVector<f64>>,
420        errors: &VecDeque<DVector<f64>>,
421        _hessians: &VecDeque<DMatrix<f64>>,
422    ) -> Result<(DVector<f64>, Vec<f64>, usize), GdiisError> {
423        let n = coords.len();
424        if n < 2 {
425            return Err(GdiisError::InsufficientHistory);
426        }
427
428        let n_use = n.min(self.max_vectors);
429        let dim = coords[0].len();
430
431        // Initialize or extend matrices
432        // Reset if matrix size doesn't match expected size (history may have changed)
433        let current_size = self.a_matrix.size();
434        if current_size == 0 || current_size > n_use || n_use > current_size + 1 {
435            // Reinitialize matrices for consistency
436            self.initialize_matrices(errors, n_use)?;
437        } else if n_use > current_size {
438            self.extend_matrices(errors, n_use)?;
439        }
440        // If n_use == current_size, matrices are already correct size
441
442        // Solve for coefficients
443        let mut coeffs = vec![0.0; n_use];
444        coeffs[0] = 1.0;
445
446        let n_used = self.solver.solve(
447            &self.a_matrix,
448            &mut self.a_inv,
449            &mut coeffs,
450            1,
451            n_use,
452        )?;
453
454        // Validate coefficients
455        self.validate_coefficients(&coeffs, n_used)?;
456
457        // Compute interpolated geometry
458        let mut x_interp = DVector::zeros(dim);
459        for (i, coord) in coords.iter().take(n_used).enumerate() {
460            x_interp += coord * coeffs[i];
461        }
462
463        // Compute interpolated error (residuum)
464        let mut e_interp = DVector::zeros(dim);
465        for (i, error) in errors.iter().take(n_used).enumerate() {
466            e_interp += error * coeffs[i];
467        }
468
469        // Validate using cosine check
470        if n_used >= 2 {
471            self.validate_cosine(&e_interp, &errors[0], n_used)?;
472        }
473
474        // Compute GDIIS step: x_new = x_interp - e_interp
475        let x_new = &x_interp - &e_interp;
476
477        Ok((x_new, coeffs[..n_used].to_vec(), n_used))
478    }
479
480    /// Initializes overlap matrices from error vectors.
481    fn initialize_matrices(
482        &mut self,
483        errors: &VecDeque<DVector<f64>>,
484        n: usize,
485    ) -> Result<(), GdiisError> {
486        self.a_matrix = TriangularMatrix::new(n);
487        self.a_inv = TriangularMatrix::new(n);
488
489        // Build overlap matrix
490        for i in 0..n {
491            for j in 0..=i {
492                let dot = errors[i].dot(&errors[j]);
493                self.a_matrix.set(i, j, dot);
494            }
495        }
496
497        // Find minimum diagonal for scaling
498        self.el2_min = (0..n)
499            .map(|i| self.a_matrix.get(i, i))
500            .fold(f64::INFINITY, f64::min);
501
502        if self.el2_min < 1e-30 {
503            return Err(GdiisError::SingularMatrix);
504        }
505
506        self.scale = 1.0 / self.el2_min;
507
508        // Scale matrix
509        self.a_matrix.scale(self.scale);
510
511        // Initialize inverse as identity (will be updated by solver)
512        for i in 0..n {
513            self.a_inv.set(i, i, 1.0);
514        }
515
516        Ok(())
517    }
518
519    /// Extends matrices with new error vector.
520    fn extend_matrices(
521        &mut self,
522        errors: &VecDeque<DVector<f64>>,
523        n: usize,
524    ) -> Result<(), GdiisError> {
525        let old_n = self.a_matrix.size();
526        
527        if n <= old_n {
528            return Ok(());
529        }
530
531        // If size mismatch is too large, reinitialize instead of extending
532        if n > old_n + 1 {
533            // Matrix state is inconsistent, reinitialize
534            return self.initialize_matrices(errors, n);
535        }
536
537        // Extend matrices by one
538        self.a_matrix.extend();
539        self.a_inv.extend();
540
541        // Add new row/column to A
542        let new_idx = n - 1;
543        for i in 0..n {
544            let dot = errors[i].dot(&errors[new_idx]);
545            self.a_matrix.set(new_idx, i, dot * self.scale);
546        }
547
548        // Check if rescaling needed
549        let el2_new = self.a_matrix.get(new_idx, new_idx) / self.scale;
550        if el2_new < self.el2_min {
551            let old_scale = self.scale;
552            self.el2_min = el2_new;
553            self.scale = 1.0 / self.el2_min;
554
555            // Rescale matrices
556            let ratio = old_scale / self.scale;
557            for i in 0..old_n {
558                for j in 0..=i {
559                    let val = self.a_matrix.get(i, j);
560                    self.a_matrix.set(i, j, val / ratio);
561                    let inv_val = self.a_inv.get(i, j);
562                    self.a_inv.set(i, j, inv_val * ratio);
563                }
564            }
565        }
566
567        // Initialize new row/column of inverse
568        self.a_inv.set(new_idx, new_idx, 1.0);
569
570        Ok(())
571    }
572
573    /// Validates DIIS coefficients.
574    ///
575    /// GDIIS coefficient check logic:
576    /// - CMax = 15.0
577    /// - CMin = 1 - CMax = -14 for IChkC != 1
578    /// - CMin = 4 * (1 - CMax) = -56 for IChkC == 1 (Regular mode)
579    fn validate_coefficients(&self, coeffs: &[f64], n_used: usize) -> Result<(), GdiisError> {
580        if self.coeff_check == CoeffCheckMode::None {
581            return Ok(());
582        }
583
584        let c_max = 15.0;
585        // Old code: If(IChkC.eq.1) CMin = Two*Two*CMin
586        // IChkC=1 corresponds to Regular mode
587        let c_min = if self.coeff_check == CoeffCheckMode::Regular {
588            4.0 * (1.0 - c_max) // -56.0
589        } else {
590            1.0 - c_max // -14.0
591        };
592
593        // Check for excessive extrapolation (sum of negative coefficients)
594        let neg_sum: f64 = coeffs[..n_used]
595            .iter()
596            .filter(|&&c| c < 0.0)
597            .sum();
598
599        if neg_sum < c_min {
600            return Err(GdiisError::CoefficientCheckFailed {
601                reason: format!("Excessive extrapolation: neg_sum = {:.4} < {:.4}", neg_sum, c_min),
602            });
603        }
604
605        // Check that last point has weight (Small = 1e-4)
606        if coeffs[0].abs() < 1e-4 {
607            return Err(GdiisError::CoefficientCheckFailed {
608                reason: "Last point has no weight".to_string(),
609            });
610        }
611
612        // For ForceRecent mode (IChkC > 1), check that recent points dominate
613        if matches!(self.coeff_check, CoeffCheckMode::ForceRecent | CoeffCheckMode::Combined) {
614            let max_idx = coeffs[..n_used]
615                .iter()
616                .enumerate()
617                .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
618                .map(|(i, _)| i)
619                .unwrap_or(0);
620
621            if max_idx != 0 {
622                // Max coefficient not at last point
623                if n_used > 2 && max_idx != 1 {
624                    // Neither of the last two points has max weight
625                    return Err(GdiisError::CoefficientCheckFailed {
626                        reason: format!("Max coefficient at index {} (not recent)", max_idx),
627                    });
628                } else if n_used == 2 {
629                    // For 2 points, Code swaps or fails based on coefficient values
630                    let cof_max = coeffs[..n_used]
631                        .iter()
632                        .map(|c| c.abs())
633                        .fold(0.0_f64, f64::max);
634                    if cof_max > 1.0 && self.coeff_check == CoeffCheckMode::ForceRecent {
635                        // Would swap, but we just fail here
636                        return Err(GdiisError::CoefficientCheckFailed {
637                            reason: "Two-point extrapolation with wrong direction".to_string(),
638                        });
639                    }
640                }
641            }
642        }
643
644        Ok(())
645    }
646
647    /// Validates GDIIS step using cosine check.
648    fn validate_cosine(
649        &self,
650        residuum: &DVector<f64>,
651        last_error: &DVector<f64>,
652        n_used: usize,
653    ) -> Result<(), GdiisError> {
654        if self.cosine_check == CosineCheckMode::None {
655            return Ok(());
656        }
657
658        let s11 = residuum.norm_squared();
659        let s22 = last_error.norm_squared();
660        let s12 = residuum.dot(last_error);
661
662        if s11 * s22 < 1e-30 {
663            return Ok(()); // Skip check if vectors too small
664        }
665
666        let cos = s12 / (s11 * s22).sqrt();
667
668        let cos_limit = match self.cosine_check {
669            CosineCheckMode::None => return Ok(()),
670            CosineCheckMode::Zero => 0.0,
671            CosineCheckMode::Standard => 0.71,
672            CosineCheckMode::Variable => variable_cos_limit(n_used),
673            CosineCheckMode::Strict => 0.866,
674        };
675
676        if cos < cos_limit {
677            Err(GdiisError::CosineCheckFailed { cos, limit: cos_limit })
678        } else {
679            Ok(())
680        }
681    }
682}
683
684/// Returns variable cosine limit based on number of vectors used.
685pub fn variable_cos_limit(n_used: usize) -> f64 {
686    match n_used {
687        2 => 0.97,
688        3 => 0.84,
689        4 => 0.71,
690        5 => 0.67,
691        6 => 0.62,
692        7 => 0.56,
693        8 => 0.49,
694        9 => 0.41,
695        _ => 0.0,
696    }
697}
698