Skip to main content

mini_ode/optimizers/
cg.rs

1use anyhow::anyhow;
2use std::fmt;
3use tch::Tensor;
4
5use super::Optimizer;
6
7use crate::utils::differentiation;
8use crate::utils::linesearch;
9use crate::utils::validation;
10use crate::utils::warnings::warn;
11
12/// Conjugate Gradient optimization algorithm
13///
14/// This struct configures the nonlinear conjugate gradient method with Polak-Ribiere+
15/// (PR+) beta and orthogonality-based restarts. It is gradient-only (first-order) and
16/// memory-efficient, suitable for large-scale problems.
17///
18/// # Fields
19/// * `max_steps` - Maximum number of optimization steps.
20/// * `gtol` - Optional tolerance for gradient norm (stop if ||grad|| < gtol).
21/// * `ftol` - Optional tolerance for change in objective value (stop if |f - prev_f| < ftol).
22pub struct CG {
23    // Maximum number of optimization steps
24    max_steps: usize,
25    // Minimum gradient
26    gtol: Option<f64>,
27    // Minimum change in the objective function between iterations
28    ftol: Option<f64>,
29}
30
31impl CG {
32    pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
33        Self {
34            max_steps,
35            gtol,
36            ftol,
37        }
38    }
39}
40
41impl Optimizer for CG {
42    /// Creates a new CG optimizer with the given parameters.
43    ///
44    /// # Arguments
45    /// * `max_steps` - Maximum iterations.
46    /// * `gtol` - Optional gradient tolerance.
47    /// * `ftol` - Optional function value change tolerance.
48    ///
49    /// # Returns
50    /// Configured CG instance.
51    fn optimize(
52        &self,
53        function: &dyn Fn(&Tensor) -> Tensor,
54        x0: &Tensor,
55    ) -> anyhow::Result<Tensor> {
56        // Ensure that rank of the initital guess tensor is 1
57        if x0.size().len() != 1 {
58            return Err(anyhow!("`x0` must have rank 1"));
59        }
60
61        let mut prev3_step_norm = 0f64;
62        let mut prev2_step_norm = 0f64;
63        let mut prev_step_norm = 0f64;
64
65        let mut prev_grad = Tensor::f_zeros_like(&x0)?;
66        let mut prev_direction = Tensor::f_zeros_like(&x0)?;
67        let mut prev_y: Option<Tensor> = None;
68        let mut x = x0.copy();
69
70        let mut warned_nonfinite_grad = false;
71        let mut warned_beta_clamp = false;
72        let mut warned_nonfinite_iter = false;
73
74        for step_num in 0..self.max_steps {
75            let grad = match differentiation::differentiate(function, &x) {
76                Ok(grad) => grad,
77                Err(e) => {
78                    return Err(anyhow!(
79                        "Runtime error: Differentiation failed in CG optimizer: {}",
80                        e
81                    ));
82                }
83            };
84
85            // Warning: non-finite gradient
86            if !warned_nonfinite_grad && grad.isfinite().f_all()?.f_int64_value(&[])? == 0 {
87                warn!("CG: non-finite gradient detected; function may be ill-defined");
88                warned_nonfinite_grad = true;
89            }
90
91            // Stop if gradient is smaller than `gtol`
92            if let Some(gtol) = self.gtol {
93                if grad.norm().f_double_value(&[])? < gtol {
94                    // Final result validation
95                    validation::validate_optimizer_output(&x, "CG")?;
96                    return Ok(x);
97                }
98            } else {
99                // This check is necessary. Continuation of the algorithm
100                // with gradient equal to exactly zero leads to NaN appearing
101                // in the result.
102                if grad.norm().f_double_value(&[])? == 0. {
103                    // Final result validation
104                    validation::validate_optimizer_output(&x, "CG")?;
105                    return Ok(x);
106                }
107            }
108
109            // Calculate direction with PR+ and orthogonality-based restart
110            let direction = match step_num {
111                0 => -&grad,
112                _ => {
113                    let orthogonality_measure = grad
114                        .f_reshape([-1])?
115                        .f_dot(&prev_grad.f_reshape([-1])?)?
116                        .f_abs()?
117                        / grad.f_reshape([-1])?.f_dot(&grad.f_reshape([-1])?)?;
118                    if orthogonality_measure.f_double_value(&[])? > 0.2 {
119                        // Restart
120                        -&grad
121                    } else {
122                        let beta = grad
123                            .f_reshape([-1])?
124                            .f_dot(&(&grad - &prev_grad).f_reshape([-1])?)?
125                            / prev_grad
126                                .f_reshape([-1])?
127                                .f_dot(&prev_grad.f_reshape([-1])?)?;
128                        // Clamp beta to be nonnegative (PR+)
129                        let beta = if beta.f_double_value(&[])? > 0. {
130                            beta
131                        } else {
132                            tch::Tensor::f_zeros_like(&beta)?
133                        };
134                        // Clamp beta to not be too large (this may result in numerical instability)
135                        let beta = if beta.f_double_value(&[])? > 1e12 {
136                            if !warned_beta_clamp {
137                                warn!("CG: beta clamped to 1e12; optimizer may be diverging");
138                                warned_beta_clamp = true;
139                            }
140                            tch::Tensor::f_ones_like(&beta)? * 1e12
141                        } else {
142                            beta
143                        };
144
145                        -&grad + beta * &prev_direction
146                    }
147                }
148            };
149
150            // Calculate linesearch_atol based on previous step norms
151            let linesearch_atol = linesearch::P0
152                .max(prev_step_norm.min(prev2_step_norm).min(prev3_step_norm) / 1000.);
153
154            // Choose step in direction `direction`
155            // Note: golden section handles non-finite gracefully during search
156            let step =
157                linesearch::choose_step_golden_section(&x, &direction, &function, linesearch_atol)?;
158
159            // Update previous step norms
160            prev3_step_norm = prev2_step_norm;
161            prev2_step_norm = prev_step_norm;
162            prev_step_norm = step.f_norm()?.f_double_value(&[])?;
163
164            // Apply step
165            x = x + step;
166
167            // Warning: non-finite iterate
168            if !warned_nonfinite_iter && x.isfinite().f_all()?.f_int64_value(&[])? == 0 {
169                warn!("CG: non-finite iterate detected; step size may be too large");
170                warned_nonfinite_iter = true;
171            }
172
173            // Stop if change in function value is smaller than `ftol`
174            let y = function(&x);
175            if let (Some(prev_y), Some(ftol)) = (prev_y, self.ftol) {
176                if (&prev_y - &y).f_double_value(&[])? < ftol {
177                    // Final result validation
178                    validation::validate_optimizer_output(&x, "CG")?;
179                    return Ok(x);
180                }
181            }
182            prev_y = Some(y);
183
184            // Update previous gradient value and previous direction value
185            prev_grad = grad;
186            prev_direction = direction;
187        }
188
189        // Final result validation
190        validation::validate_optimizer_output(&x, "CG")?;
191        Ok(x)
192    }
193}
194
195impl fmt::Display for CG {
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        let mut string = String::from("CG(");
198
199        string = string + "max_steps=" + self.max_steps.to_string().as_str();
200        if let Some(gtol) = self.gtol {
201            string = string + ", gtol=" + gtol.to_string().as_str();
202        }
203        if let Some(ftol) = self.ftol {
204            string = string + ", ftol=" + ftol.to_string().as_str();
205        }
206        string = string + ")";
207
208        write!(f, "{}", string)
209    }
210}