Skip to main content

mini_ode/optimizers/
newton.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/// Newton optimization algorithm
13///
14/// This struct configures the Newton method, a second-order optimizer that uses the
15/// Hessian matrix for quadratic approximations.
16///
17/// # Fields
18/// * `max_steps` - Maximum number of optimization steps.
19/// * `gtol` - Optional tolerance for gradient norm (stop if ||grad|| < gtol).
20/// * `ftol` - Optional tolerance for change in objective value (stop if |f - prev_f| < ftol).
21pub struct Newton {
22    // Maximum number of optimization steps
23    max_steps: usize,
24    // Minimum gradient
25    gtol: Option<f64>,
26    // minimum change in the objective function between iterations
27    ftol: Option<f64>,
28}
29
30impl Newton {
31    /// Creates a new Newton optimizer with the given parameters.
32    ///
33    /// # Arguments
34    /// * `max_steps` - Maximum iterations.
35    /// * `gtol` - Optional gradient tolerance.
36    /// * `ftol` - Optional function value change tolerance.
37    ///
38    /// # Returns
39    /// Configured Newton instance.
40    pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
41        Self {
42            max_steps,
43            gtol,
44            ftol,
45        }
46    }
47}
48
49impl Optimizer for Newton {
50    fn optimize(
51        &self,
52        function: &dyn Fn(&Tensor) -> Tensor,
53        x0: &Tensor,
54    ) -> anyhow::Result<Tensor> {
55        // Ensure that rank of the initital guess tensor is 1
56        if x0.size().len() != 1 {
57            return Err(anyhow!("`x0` must have rank 1"));
58        }
59
60        // Determine the device and kind for use in the function
61        let kind = x0.kind();
62        let device = x0.device();
63
64        let x0_length = x0.size()[0];
65
66        // Test for sufficient resources for storing Hessian
67        let _ = match Tensor::f_eye(x0_length, (kind, device)) {
68            Ok(matrix) => matrix,
69            // Give knowledgable error message to the user
70            // when there is unsufficient memory.
71            Err(tch::TchError::Torch(_)) => {
72                return Err(anyhow!(
73                    "Could not allocate {}x{} matrix. Maybe try less resourcefull algorithm.",
74                    x0_length,
75                    x0_length
76                ));
77            }
78            e => e.unwrap(),
79        };
80
81        let mut x = x0.copy();
82        let mut curr_y = function(&x);
83
84        // Ensure that output of `function` is a scalar
85        if curr_y.size() != Vec::<i64>::new() {
86            return Err(anyhow!("Output of function `function` must be scalar"));
87        }
88
89        let mut warned_damping_moderate = false;
90        let mut warned_damping_severe = false;
91
92        for _ in 0..self.max_steps {
93            let (curr_grad, curr_hessian) =
94                match differentiation::gradient_and_hessian(function, &x) {
95                    Ok(gh) => gh,
96                    Err(e) => {
97                        return Err(anyhow!(
98                            "Runtime error: Differentiation failed in Newton optimizer: {}",
99                            e
100                        ));
101                    }
102                };
103
104            // Check for stop condition
105            if let Some(gtol) = self.gtol {
106                if curr_grad.f_norm()?.f_double_value(&[])? < gtol {
107                    // Final result validation
108                    validation::validate_optimizer_output(&x, "Newton")?;
109                    return Ok(x);
110                }
111            } else {
112                // This check is necessary. Continuation of the algorithm
113                // with gradient equal to exactly zero leads to NaN appearing
114                // in the result.
115                if curr_grad.f_norm()?.f_double_value(&[])? == 0. {
116                    // Final result validation
117                    validation::validate_optimizer_output(&x, "Newton")?;
118                    return Ok(x);
119                }
120            }
121
122            // Calculate step direction
123            let negative_grad = -curr_grad.f_reshape([-1, 1])?; // Negative gradient direction
124            let mut lambda = (negative_grad.f_norm()?.f_double_value(&[])? * 1e-3).max(1e-8); // Initial dampening factor
125            let direction = loop {
126                // We damp hessian until it is positive definite.
127                // For non-positive definite Hessian, Newton method may give unwanted results.
128                let damped_hessian =
129                    &curr_hessian + Tensor::f_eye(x0_length, (kind, device))? * lambda;
130
131                // Try to perform Banach-Cholesky decomposition of damped hessian
132                match damped_hessian.f_linalg_cholesky(false) {
133                    Ok(lower_triangular) => {
134                        // Hessian is positive-definite. Solve system with Banach-Cholesky decomposition
135                        let y = lower_triangular.f_linalg_solve_triangular(
136                            &negative_grad,
137                            false,
138                            true,
139                            false,
140                        )?;
141                        break lower_triangular
142                            .f_transpose(0, 1)?
143                            .f_linalg_solve_triangular(&y, true, true, false)?
144                            .reshape([-1]);
145                    }
146                    Err(_) => {
147                        // Hessian is not positive-definite. Try increasing dampening factor.
148                        lambda *= 10.;
149
150                        // Warnings for damping levels
151                        if !warned_damping_moderate && lambda >= 1e3 && lambda < 1e7 {
152                            warn!(
153                                "Newton: Hessian required damping factor {:.3e}; problem may be ill-conditioned",
154                                lambda
155                            );
156                            warned_damping_moderate = true;
157                        }
158
159                        if !warned_damping_severe && lambda >= 1e10 {
160                            warn!(
161                                "Newton: Hessian damping factor reached {:.3e}; falling back to pseudoinverse (Hessian is severely ill-conditioned)",
162                                lambda
163                            );
164                            warned_damping_severe = true;
165                        }
166
167                        if lambda > 1e10 {
168                            // Dampening factor (lambda) exceeded maximum value. Fallback to pseudoinverse.
169                            break curr_hessian
170                                .f_linalg_pinv(1e-14, false)?
171                                .f_mm(&negative_grad)?
172                                .f_reshape([-1])?;
173                        }
174                    }
175                }
176            };
177
178            // Choose optimal step in given direction using line search
179            // Backtracking handles non-finite gracefully during search
180            let step = linesearch::choose_step_backtracking(
181                &x, &direction, function, &curr_grad, 0.1, 0.9,
182            )?;
183
184            // Apply step
185            x = x + &step;
186
187            // Check for stop contition
188            let y = function(&x);
189            if let Some(ftol) = self.ftol {
190                if (curr_y.f_double_value(&[])? - y.f_double_value(&[])?) < ftol {
191                    // Final result validation
192                    validation::validate_optimizer_output(&x, "Newton")?;
193                    return Ok(x);
194                }
195            }
196            curr_y = y;
197        }
198
199        // Final result validation
200        validation::validate_optimizer_output(&x, "Newton")?;
201        Ok(x)
202    }
203}
204
205impl fmt::Display for Newton {
206    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207        let mut string = String::from("Newton(");
208
209        string = string + "max_steps=" + self.max_steps.to_string().as_str();
210        if let Some(gtol) = self.gtol {
211            string = string + ", gtol=" + gtol.to_string().as_str();
212        }
213        if let Some(ftol) = self.ftol {
214            string = string + ", ftol=" + ftol.to_string().as_str();
215        }
216
217        string = string + ")";
218
219        write!(f, "{}", string)
220    }
221}