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}