Skip to main content

mini_ode/optimizers/
halley.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/// Halley optimization algorithm
13///
14/// This struct configures the Halley method, a third-order optimizer that uses
15/// tensor of third order derivatives.
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 Halley {
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 Halley {
31    /// Creates a new Halley 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 Halley 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 Halley {
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 tensor of third order derivatives
67        let _ = match Tensor::f_zeros([x0_length, x0_length, 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{}x{} tensor. Maybe try less resourcefull algorithm.",
74                    x0_length,
75                    x0_length,
76                    x0_length
77                ));
78            }
79            e => e.unwrap(),
80        };
81
82        let mut x = x0.copy();
83        let mut curr_y = function(&x);
84
85        // Ensure that output of `function` is a scalar
86        if curr_y.size() != Vec::<i64>::new() {
87            return Err(anyhow!("Output of function `function` must be scalar"));
88        }
89
90        let mut warned_pinv_large = false;
91
92        for _ in 0..self.max_steps {
93            let (curr_grad, curr_hessian, curr_d3_tensor) =
94                match differentiation::derivative_tensors_123(function, &x) {
95                    Ok(ghd3) => ghd3,
96                    Err(e) => {
97                        return Err(anyhow!(
98                            "Runtime error: Differentiation failed in Halley 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, "Halley")?;
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, "Halley")?;
118                    return Ok(x);
119                }
120            }
121
122            // Calculate step direction
123            let hessian_pinv = curr_hessian.f_linalg_pinv(1e-14, false)?;
124
125            // Warning: large pseudoinverse norm
126            let pinv_norm = hessian_pinv.f_norm()?.f_double_value(&[])?;
127            if !warned_pinv_large && pinv_norm > 1e8 {
128                warn!(
129                    "Halley: Hessian pseudoinverse norm is {:.3e}; Hessian may be ill-conditioned",
130                    pinv_norm
131                );
132                warned_pinv_large = true;
133            }
134
135            let neg_newton_dir = hessian_pinv.f_mm(&curr_grad.f_reshape([-1, 1])?)?;
136            let direction = -hessian_pinv
137                .f_mm(
138                    &(curr_grad.f_reshape([-1, 1])?
139                        + curr_d3_tensor
140                            .f_matmul(&neg_newton_dir)?
141                            .f_reshape([x0_length, x0_length])?
142                            .f_mm(&neg_newton_dir)?
143                            * 0.5),
144                )?
145                .f_reshape([-1])?;
146
147            // Choose optimal step in given direction using line search
148            // Backtracking handles non-finite gracefully during search
149            let step = linesearch::choose_step_backtracking(
150                &x, &direction, function, &curr_grad, 0.1, 0.9,
151            )?;
152
153            // Apply step
154            x = x + &step;
155
156            // Check for stop contition
157            let y = function(&x);
158            if let Some(ftol) = self.ftol {
159                if (curr_y.f_double_value(&[])? - y.f_double_value(&[])?) < ftol {
160                    // Final result validation
161                    validation::validate_optimizer_output(&x, "Halley")?;
162                    return Ok(x);
163                }
164            }
165            curr_y = y;
166        }
167
168        // Final result validation
169        validation::validate_optimizer_output(&x, "Halley")?;
170        Ok(x)
171    }
172}
173
174impl fmt::Display for Halley {
175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176        let mut string = String::from("Halley(");
177
178        string = string + "max_steps=" + self.max_steps.to_string().as_str();
179        if let Some(gtol) = self.gtol {
180            string = string + ", gtol=" + gtol.to_string().as_str();
181        }
182        if let Some(ftol) = self.ftol {
183            string = string + ", ftol=" + ftol.to_string().as_str();
184        }
185
186        string = string + ")";
187
188        write!(f, "{}", string)
189    }
190}