mini_ode/optimizers.rs
1//! Nonlinear optimization algorithms which are required by some ODE solvers
2//!
3//! The user may create objects containing optimizer configuration and pass it to ODE solver.
4
5use anyhow::anyhow;
6use std::fmt;
7use tch::{IndexOp, Tensor};
8
9/// Optimizer interface common for any optimizer in the library
10pub trait Optimizer: Send + Sync + fmt::Display {
11 /// Solves the problem of optimization of function `function` starting from point `x0`
12 ///
13 /// # Arguments
14 /// * `function` - A closure that takes a 1D tensor `x` and returns a scalar tensor.
15 /// representing the objective value to minimize.
16 /// * `x0` - Initial guess, 1D tensor accepted by `function`.
17 ///
18 /// # Returns
19 /// Optimal `x`, or error if optimization fails.
20 ///
21 /// # Panics
22 /// May panic if libtorch tensor operations fail.
23 fn optimize(&self, function: &dyn Fn(&Tensor) -> Tensor, x0: &Tensor)
24 -> anyhow::Result<Tensor>;
25}
26
27/// Newton optimization algorithm
28///
29/// This struct configures the Newton method, a second-order optimizer that uses the
30/// Hessian matrix for quadratic approximations.
31///
32/// # Fields
33/// * `max_steps` - Maximum number of optimization steps.
34/// * `gtol` - Optional tolerance for gradient norm (stop if ||grad|| < gtol).
35/// * `ftol` - Optional tolerance for change in objective value (stop if |f - prev_f| < ftol).
36pub struct Newton {
37 // Maximum number of optimization steps
38 max_steps: usize,
39 // Minimum gradient
40 gtol: Option<f64>,
41 // minimum change in the objective function between iterations
42 ftol: Option<f64>,
43}
44
45/// Broyden-Fletcher-Goldfarb-Shanno optimization algorithm
46///
47/// This struct configures the BFGS quasi-Newton method, which approximates the inverse
48/// Hessian using rank-2 updates. It is as memory-intensive as regular Newton method
49/// (O(n^2) storage) but it does not require double differentiation.
50///
51/// # Fields
52/// * `max_steps` - Maximum number of optimization steps.
53/// * `gtol` - Optional tolerance for gradient norm (stop if ||grad|| < gtol).
54/// * `ftol` - Optional tolerance for change in objective value (stop if |f - prev_f| < ftol).
55pub struct BFGS {
56 // Maximum number of optimization steps
57 max_steps: usize,
58 // Minimum gradient
59 gtol: Option<f64>,
60 // minimum change in the objective function between iterations
61 ftol: Option<f64>,
62}
63
64/// Halley optimization algorithm
65///
66/// This struct configures the Halley method, a third-order optimizer that uses
67/// tensor of third order derivatives.
68///
69/// # Fields
70/// * `max_steps` - Maximum number of optimization steps.
71/// * `gtol` - Optional tolerance for gradient norm (stop if ||grad|| < gtol).
72/// * `ftol` - Optional tolerance for change in objective value (stop if |f - prev_f| < ftol).
73pub struct Halley {
74 // Maximum number of optimization steps
75 max_steps: usize,
76 // Minimum gradient
77 gtol: Option<f64>,
78 // minimum change in the objective function between iterations
79 ftol: Option<f64>,
80}
81
82/// Conjugate Gradient optimization algorithm
83///
84/// This struct configures the nonlinear conjugate gradient method with Polak-Ribiere+
85/// (PR+) beta and orthogonality-based restarts. It is gradient-only (first-order) and
86/// memory-efficient, suitable for large-scale problems.
87///
88/// # Fields
89/// * `max_steps` - Maximum number of optimization steps.
90/// * `gtol` - Optional tolerance for gradient norm (stop if ||grad|| < gtol).
91/// * `ftol` - Optional tolerance for change in objective value (stop if |f - prev_f| < ftol).
92pub struct CG {
93 // Maximum number of optimization steps
94 max_steps: usize,
95 // Minimum gradient
96 gtol: Option<f64>,
97 // Minimum change in the objective function between iterations
98 ftol: Option<f64>,
99}
100
101/// Computes the gradient of `function` at `x` using automatic differentiation.
102///
103/// # Arguments
104/// * `function` - A closure that takes a 1D tensor `x` and returns a scalar tensor.
105/// * `x` - Evaluation point (1D tensor).
106///
107/// # Returns
108/// Gradient tensor at `x`.
109pub(crate) fn differentiate(function: &dyn Fn(&Tensor) -> Tensor, x: &Tensor) -> Tensor {
110 let x_with_grad = x.detach().copy().set_requires_grad(true);
111 let y = function(&x_with_grad);
112
113 tch::Tensor::run_backward(&[y], &[x_with_grad], false, false)[0].copy()
114}
115
116/// Computes the gradient and Hessian of `function` at `x` using automatic differentiation.
117///
118/// # Arguments
119/// * `function` - A closure that takes a 1D tensor `x` and returns a scalar tensor.
120/// * `x` - Evaluation point (1D tensor).
121/// # Returns
122/// Tuple `(grad, hessian)`, both detached tensors. `grad` is 1D, `hessian` is 2D.
123pub(crate) fn gradient_and_hessian(function: &dyn Fn(&Tensor) -> Tensor, x: &Tensor) -> (Tensor, Tensor) {
124 let x_with_grad = x.detach().copy().set_requires_grad(true);
125 let y = function(&x_with_grad);
126
127 // keep_graph = true (this graph is needed for some functions during second differentiation)
128 // create_graph = true (allow calculating second derivatives)
129 let grad = Tensor::run_backward(&[y], &[&x_with_grad], true, true)[0].copy();
130 let grad_len = grad.size()[0];
131 let grad_kind = grad.kind();
132 let grad_device = grad.device();
133
134 // If gradient is constant, immediately return gradient and zero hessian
135 // It is not possible to differentiate constants in torch
136 if !grad.requires_grad() {
137 return (grad, Tensor::zeros([grad_len, grad_len], (grad_kind, grad_device)));
138 }
139
140 let mut vectors = Vec::<Tensor>::with_capacity(grad_len as usize);
141 for i in 0..grad_len {
142 // keep_graph = true (we need to run backward pass multiple times - in each iteration of the loop)
143 // create_graph = false (we don't need to differentiate three times)
144 vectors.append(&mut Tensor::run_backward(
145 &[grad.i(i)],
146 &[&x_with_grad],
147 true,
148 false,
149 ));
150 }
151
152 // Detach autograd computation graph
153 let grad = grad.detach();
154 // Stack slices of the Hessian matrix and detach autograd computation graph
155 let hessian = Tensor::stack(&vectors, 0).detach();
156
157 (grad, hessian)
158}
159
160/// Computes the gradient, Hessian and third derivatives tensor of `function` at `x` using automatic differentiation.
161///
162/// # Arguments
163/// * `function` - A closure that takes a 1D tensor `x` and returns a scalar tensor.
164/// * `x` - Evaluation point (1D tensor).
165/// # Returns
166/// Tuple `(grad, hessian, d3_tensor)`, both detached tensors. `grad` is 1D, `hessian` is 2D, `d3_tensor` is 3D.
167pub(crate) fn derivative_tensors_123(function: &dyn Fn(&Tensor) -> Tensor, x: &Tensor) -> (Tensor, Tensor, Tensor) {
168 let x_with_grad = x.detach().copy().set_requires_grad(true);
169 let y = function(&x_with_grad);
170
171 // keep_graph = true (this graph is needed for some functions during second differentiation)
172 // create_graph = true (allow calculating second derivatives)
173 let grad = Tensor::run_backward(&[y], &[&x_with_grad], true, true)[0].copy();
174 let grad_len = grad.size()[0];
175 let grad_kind = grad.kind();
176 let grad_device = grad.device();
177
178 // If gradient is constant, immediately return gradient zero hessian and
179 // zero tensor of third order derivatives
180 // It is not possible to differentiate constants in torch
181 if !grad.requires_grad() {
182 return (
183 grad,
184 Tensor::zeros([grad_len, grad_len], (grad_kind, grad_device)),
185 Tensor::zeros([grad_len, grad_len, grad_len], (grad_kind, grad_device))
186 );
187 }
188
189 let mut vectors = Vec::<Tensor>::with_capacity(grad_len as usize);
190 for i in 0..grad_len {
191 // keep_graph = true (we need to run backward pass multiple times - in each iteration of the loop)
192 // create_graph = true (we need to differentiate three times)
193 vectors.append(&mut Tensor::run_backward(
194 &[grad.i(i)],
195 &[&x_with_grad],
196 true,
197 true,
198 ));
199 }
200
201 // Stack slices of the Hessian matrix
202 let hessian = Tensor::stack(&vectors, 0);
203
204 // If gradient is constant, immediately return gradient zero hessian and
205 // zero tensor of third order derivatives
206 // It is not possible to differentiate constants in torch
207 if !hessian.requires_grad() {
208 return (
209 grad,
210 hessian,
211 Tensor::zeros([grad_len, grad_len, grad_len], (grad_kind, grad_device))
212 );
213 }
214
215 let mut vectors2 = Vec::<Tensor>::with_capacity(grad_len as usize);
216 for i in 0..grad_len {
217 let mut vectors1 = Vec::<Tensor>::with_capacity(grad_len as usize);
218 for j in 0..grad_len {
219 vectors1.append(&mut Tensor::run_backward(
220 &[hessian.i((i, j))],
221 &[&x_with_grad],
222 true,
223 false,
224 ));
225 }
226 vectors2.push(Tensor::stack(&vectors1, 0));
227 }
228
229 // Detach autograd computation graph
230 let grad = grad.detach();
231 let hessian = hessian.detach();
232 // Stack slices of the tensor of third derivatives and detach autograd computation graph
233 let d3_tensor = Tensor::stack(&vectors2, 0).detach();
234
235 (grad, hessian, d3_tensor)
236}
237
238/// Minimum step value.
239const P0: f64 = 0.0000000001f64;
240
241/// Golden ratio squared (phi^2)
242const PHI2: f64 = 2.618033988749894848207f64;
243/// Reciprocal of golden ratio (1/phi)
244const RPHI: f64 = 0.618033988749894848207f64;
245
246/// Performs a golden section line search to find a step size that approximately minimizes
247/// `function` along `direction` from `x0`, subject to tolerance `atol`.
248///
249/// # Arguments
250/// * `x0` - Starting point (1D tensor).
251/// * `direction` - Search direction (1D tensor).
252/// * `function` - Objective function.
253/// * `atol` - Absolute tolerance for step size convergence.
254///
255/// # Returns
256/// Optimal step tensor.
257fn choose_step_golden_section(
258 x0: &Tensor,
259 direction: &Tensor,
260 function: &dyn Fn(&Tensor) -> Tensor,
261 atol: f64,
262) -> Tensor {
263 let (mut x1, mut x2, mut x3, mut x4): (f64, f64, f64, f64);
264 let (fx1, mut fx3, mut fx4): (f64, f64, f64);
265
266 fx1 = function(&x0).double_value(&[]);
267
268 x1 = 0.;
269 // Heuristics: Try to set x2 based on atol value. If we succeed, we can
270 // skip some forward search iterations.
271 let fx_guess = function(&(x0 + direction * atol * 15.)).double_value(&[]);
272 x2 = if !fx_guess.is_finite() || fx_guess > fx1 {
273 P0
274 } else {
275 atol * 15.
276 };
277 // Forward search
278 let mut fx = function(&(x0 + direction * x2)).double_value(&[]);
279 while fx <= fx1 {
280 let new_x2 = x1 + (x2 - x1) * PHI2;
281 fx = function(&(x0 + direction * new_x2)).double_value(&[]);
282 if !fx.is_finite() {
283 break;
284 }
285 x2 = new_x2;
286 }
287
288 x3 = x2 - (x2 - x1) * RPHI;
289 x4 = x1 + (x2 - x1) * RPHI;
290 fx3 = function(&(x0 + direction * x3)).double_value(&[]);
291 fx4 = function(&(x0 + direction * x4)).double_value(&[]);
292 while x2 - x1 > atol {
293 if fx3 < fx4 {
294 x2 = x4;
295
296 fx4 = fx3;
297 x3 = x2 - (x2 - x1) * RPHI;
298 x4 = x1 + (x2 - x1) * RPHI;
299 fx3 = function(&(x0 + direction * x3)).double_value(&[]);
300 } else {
301 x1 = x3;
302
303 fx3 = fx4;
304 x3 = x2 - (x2 - x1) * RPHI;
305 x4 = x1 + (x2 - x1) * RPHI;
306 fx4 = function(&(x0 + direction * x4)).double_value(&[]);
307 }
308 }
309
310 direction * ((x1 + x2) / 2.)
311}
312
313/// Performs a backtracking line search to find a step size satisfying the Armijo condition.
314///
315/// # Arguments
316/// * `x0` - Starting point (1D tensor).
317/// * `direction` - Descent direction (1D tensor).
318/// * `function` - Objective function.
319/// * `grad` - Gradient at `x0`.
320/// * `alpha` - Armijo parameter (0 < alpha < 1, 0.1 is recommended).
321/// * `beta` - Backtracking factor (0 < beta < 1, 0.9 is recommended).
322///
323/// # Returns
324/// Step tensor.
325fn choose_step_backtracking(
326 x0: &Tensor,
327 direction: &Tensor,
328 function: &dyn Fn(&Tensor) -> Tensor,
329 grad: &Tensor,
330 alpha: f64,
331 beta: f64,
332) -> Tensor {
333 let fx0 = function(&x0).double_value(&[]);
334
335 let mut t = 1f64;
336 while {
337 let fx = function(&(x0 + direction * t)).double_value(&[]);
338
339 if !fx.is_finite() {
340 true
341 } else {
342 fx > fx0 + grad.reshape([-1]).dot(&direction.reshape([-1])).double_value(&[]) * alpha * t
343 }
344 }
345 {
346 t *= beta;
347 }
348
349 direction.copy() * t
350}
351
352impl CG {
353 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
354 Self {
355 max_steps,
356 gtol,
357 ftol,
358 }
359 }
360}
361
362impl Optimizer for CG {
363 /// Creates a new CG optimizer with the given parameters.
364 ///
365 /// # Arguments
366 /// * `max_steps` - Maximum iterations.
367 /// * `gtol` - Optional gradient tolerance.
368 /// * `ftol` - Optional function value change tolerance.
369 ///
370 /// # Returns
371 /// Configured CG instance.
372 fn optimize(
373 &self,
374 function: &dyn Fn(&Tensor) -> Tensor,
375 x0: &Tensor,
376 ) -> anyhow::Result<Tensor> {
377 // Ensure that rank of the initital guess tensor is 1
378 if x0.size().len() != 1 {
379 return Err(anyhow!("`x0` must have rank 1"));
380 }
381
382 let mut prev3_step_norm = 0f64;
383 let mut prev2_step_norm = 0f64;
384 let mut prev_step_norm = 0f64;
385
386 let mut prev_grad = Tensor::zeros_like(&x0);
387 let mut prev_direction = Tensor::zeros_like(&x0);
388 let mut prev_y: Option<Tensor> = None;
389 let mut x = x0.copy();
390
391 for step_num in 0..self.max_steps {
392 let grad = differentiate(function, &x);
393
394 // Stop if gradient is smaller than `gtol`
395 if let Some(gtol) = self.gtol {
396 if grad.norm().double_value(&[]) < gtol {
397 return Ok(x);
398 }
399 } else {
400 // This check is necessary. Continuation of the algorithm
401 // with gradient equal to exactly zero leads to NaN appearing
402 // in the result.
403 if grad.norm().double_value(&[]) == 0. {
404 return Ok(x);
405 }
406 }
407
408 // Calculate direction with PR+ and orthogonality-based restart
409 let direction = match step_num {
410 0 => -&grad,
411 _ => {
412 let orthogonality_measure = grad.reshape([-1]).dot(&prev_grad.reshape([-1])).abs()
413 / grad.reshape([-1]).dot(&grad.reshape([-1]));
414 if orthogonality_measure.double_value(&[]) > 0.2 {
415 // Restart
416 -&grad
417 } else {
418 let beta = grad.reshape([-1]).dot(&(&grad - &prev_grad).reshape([-1]))
419 / prev_grad.reshape([-1]).dot(&prev_grad.reshape([-1]));
420 // Clamp beta to be nonnegative (PR+)
421 let beta = if beta.double_value(&[]) > 0. {
422 beta
423 } else {
424 tch::Tensor::zeros_like(&beta)
425 };
426 // Clamp beta to not be too large (this may result in numerical instability)
427 let beta = if beta.double_value(&[]) > 1000000000000. {
428 tch::Tensor::ones_like(&beta) * 1000000000000.
429 } else {
430 beta
431 };
432
433 -&grad + beta * &prev_direction
434 }
435 }
436 };
437
438 // Calculate linesearch_atol based on previous step norms
439 let linesearch_atol =
440 P0.max(prev_step_norm.min(prev2_step_norm).min(prev3_step_norm) / 1000.);
441
442 // Choose step in direction `direction`
443 let step = choose_step_golden_section(&x, &direction, &function, linesearch_atol);
444
445 // Update previous step norms
446 prev3_step_norm = prev2_step_norm;
447 prev2_step_norm = prev_step_norm;
448 prev_step_norm = step.norm().double_value(&[]);
449
450 // Apply step
451 x = x + step;
452
453 // Stop if change in function value is smaller than `ftol`
454 let y = function(&x);
455 if let (Some(prev_y), Some(ftol)) = (prev_y, self.ftol) {
456 if (&prev_y - &y).double_value(&[]) < ftol {
457 return Ok(x);
458 }
459 }
460 prev_y = Some(y);
461
462 // Update previous gradient value and previous direction value
463 prev_grad = grad;
464 prev_direction = direction;
465 }
466
467 Ok(x)
468 }
469}
470
471impl fmt::Display for CG {
472 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
473 let mut string = String::from("CG(");
474
475 string = string + "max_steps=" + self.max_steps.to_string().as_str();
476 if let Some(gtol) = self.gtol {
477 string = string + ", gtol=" + gtol.to_string().as_str();
478 }
479 if let Some(ftol) = self.ftol {
480 string = string + ", ftol=" + ftol.to_string().as_str();
481 }
482 string = string + ")";
483
484 write!(f, "{}", string)
485 }
486}
487
488impl BFGS {
489 /// Creates a new BFGS optimizer with the given parameters.
490 ///
491 /// # Arguments
492 /// * `max_steps` - Maximum iterations.
493 /// * `gtol` - Optional gradient tolerance.
494 /// * `ftol` - Optional function value change tolerance.
495 ///
496 /// # Returns
497 /// Configured BFGS instance.
498 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
499 Self {
500 max_steps,
501 gtol,
502 ftol,
503 }
504 }
505}
506
507impl Optimizer for BFGS {
508 fn optimize(
509 &self,
510 function: &dyn Fn(&Tensor) -> Tensor,
511 x0: &Tensor,
512 ) -> anyhow::Result<Tensor> {
513 // Ensure that rank of the initital guess tensor is 1
514 if x0.size().len() != 1 {
515 return Err(anyhow!("`x0` must have rank 1"));
516 }
517
518 // Determine the device and kind for use in the function
519 let kind = x0.kind();
520 let device = x0.device();
521
522 let mut prev3_step_norm = 0f64;
523 let mut prev2_step_norm = 0f64;
524 let mut prev_step_norm = 0f64;
525
526 let x0_length = x0.size()[0];
527 let identity = match Tensor::f_eye(x0_length, (kind, device)) {
528 Ok(matrix) => matrix,
529 // BFGS requires a lot of resources.
530 // Give knowledgable error message to the user
531 // when BFGS fails due to unsufficient memory.
532 Err(tch::TchError::Torch(_)) => {
533 return Err(anyhow!(
534 "Could not allocate {}x{} matrix. Maybe try less resourcefull algorithm.",
535 x0_length,
536 x0_length
537 ));
538 }
539 e => e.unwrap(),
540 };
541 let mut x = x0.copy();
542 let mut appr_inv_h = identity.copy();
543 let mut curr_grad = differentiate(function, &x);
544 let mut curr_y = function(&x);
545
546 // Ensure that output of `function` is a scalar
547 if curr_y.size() != Vec::<i64>::new() {
548 return Err(anyhow!("Output of function `function` must be scalar"));
549 }
550
551 for _ in 0..self.max_steps {
552 // Check for stop condition
553 if let Some(gtol) = self.gtol {
554 if curr_grad.norm().double_value(&[]) < gtol {
555 return Ok(x);
556 }
557 } else {
558 // This check is necessary. Continuation of the algorithm
559 // with gradient equal to exactly zero leads to NaN appearing
560 // in the result.
561 if curr_grad.norm().double_value(&[]) == 0. {
562 return Ok(x);
563 }
564 }
565
566 // Calculate step direction base on the gradient and approximate hessian
567 let direction = (-appr_inv_h.mm(&curr_grad.reshape([-1, 1]))).reshape([-1]);
568
569 // Calculate linesearch_atol based on previous step norms
570 let linesearch_atol =
571 P0.max(prev_step_norm.min(prev2_step_norm).min(prev3_step_norm) / 100.);
572
573 // Choose optimal step in given direction using line search
574 let step = choose_step_golden_section(&x, &direction, function, linesearch_atol);
575
576 // Update previous step norms
577 prev3_step_norm = prev2_step_norm;
578 prev2_step_norm = prev_step_norm;
579 prev_step_norm = step.norm().double_value(&[]);
580
581 // Apply step
582 x = x + &step;
583
584 // Check for stop contition
585 let y = function(&x);
586 if let Some(ftol) = self.ftol {
587 if (curr_y.double_value(&[]) - y.double_value(&[])) < ftol {
588 return Ok(x);
589 }
590 }
591 curr_y = y;
592
593 let grad = differentiate(function, &x);
594 let gdiff = &grad - &curr_grad;
595
596 // Use Powell's dampening for gamma computation
597 // This prevents gamma from blowing up. Normal formula for gamma is 1/step.dot(gdiff)
598 let gamma = {
599 let delta = 0.0001;
600
601 let sty = step.dot(&gdiff).double_value(&[]);
602 let step_norm_sq = step.dot(&step).double_value(&[]);
603
604 let theta = if sty >= delta * step_norm_sq {
605 1.
606 } else {
607 let numerator = (1. - delta) * step_norm_sq;
608 let denominator = step_norm_sq - sty;
609
610 if denominator.abs() < 1e-10 {
611 1.
612 } else {
613 (numerator / denominator).min(1.)
614 }
615 };
616
617 let projection_factor = if step_norm_sq < 1e-10 {
618 0.
619 } else {
620 sty / step_norm_sq
621 };
622 let gdiff_prime = &gdiff * theta + &step * ((1. - theta) * projection_factor);
623 let sty_prime = step.dot(&gdiff_prime).double_value(&[]);
624
625 if sty_prime.abs() < 1e-10 {
626 1. / (delta * step_norm_sq + 1e-10)
627 } else {
628 1. / sty_prime
629 }
630 };
631
632 // Compute approximation of inverse Hessian
633 appr_inv_h = (&identity - gamma * step.reshape([-1, 1]).mm(&gdiff.reshape([1, -1])))
634 .mm(&appr_inv_h)
635 .mm(&(&identity - gamma * gdiff.reshape([-1, 1]).mm(&step.reshape([1, -1]))))
636 + gamma * step.reshape([-1, 1]).mm(&step.reshape([1, -1]));
637
638 curr_grad = grad;
639 }
640
641 Ok(x)
642 }
643}
644
645impl fmt::Display for BFGS {
646 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
647 let mut string = String::from("BFGS(");
648
649 string = string + "max_steps=" + self.max_steps.to_string().as_str();
650 if let Some(gtol) = self.gtol {
651 string = string + ", gtol=" + gtol.to_string().as_str();
652 }
653 if let Some(ftol) = self.ftol {
654 string = string + ", ftol=" + ftol.to_string().as_str();
655 }
656
657 string = string + ")";
658
659 write!(f, "{}", string)
660 }
661}
662
663impl Newton {
664 /// Creates a new Newton optimizer with the given parameters.
665 ///
666 /// # Arguments
667 /// * `max_steps` - Maximum iterations.
668 /// * `gtol` - Optional gradient tolerance.
669 /// * `ftol` - Optional function value change tolerance.
670 ///
671 /// # Returns
672 /// Configured Newton instance.
673 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
674 Self {
675 max_steps,
676 gtol,
677 ftol,
678 }
679 }
680}
681
682impl Optimizer for Newton {
683 fn optimize(
684 &self,
685 function: &dyn Fn(&Tensor) -> Tensor,
686 x0: &Tensor,
687 ) -> anyhow::Result<Tensor> {
688 // Ensure that rank of the initital guess tensor is 1
689 if x0.size().len() != 1 {
690 return Err(anyhow!("`x0` must have rank 1"));
691 }
692
693 // Determine the device and kind for use in the function
694 let kind = x0.kind();
695 let device = x0.device();
696
697 let x0_length = x0.size()[0];
698
699 // Test for sufficient resources for storing Hessian
700 let _ = match Tensor::f_eye(x0_length, (kind, device)) {
701 Ok(matrix) => matrix,
702 // Give knowledgable error message to the user
703 // when there is unsufficient memory.
704 Err(tch::TchError::Torch(_)) => {
705 return Err(anyhow!(
706 "Could not allocate {}x{} matrix. Maybe try less resourcefull algorithm.",
707 x0_length,
708 x0_length
709 ));
710 }
711 e => e.unwrap(),
712 };
713
714 let mut x = x0.copy();
715 let mut curr_y = function(&x);
716
717 // Ensure that output of `function` is a scalar
718 if curr_y.size() != Vec::<i64>::new() {
719 return Err(anyhow!("Output of function `function` must be scalar"));
720 }
721
722 for _ in 0..self.max_steps {
723 let (curr_grad, curr_hessian) = gradient_and_hessian(function, &x);
724
725 // Check for stop condition
726 if let Some(gtol) = self.gtol {
727 if curr_grad.norm().double_value(&[]) < gtol {
728 return Ok(x);
729 }
730 } else {
731 // This check is necessary. Continuation of the algorithm
732 // with gradient equal to exactly zero leads to NaN appearing
733 // in the result.
734 if curr_grad.norm().double_value(&[]) == 0. {
735 return Ok(x);
736 }
737 }
738
739 // Calculate step direction
740 let negative_grad = -curr_grad.reshape([-1, 1]); // Negative gradient direction
741 let mut lambda = (negative_grad.norm().double_value(&[]) * 1e-3).max(1e-8); // Initial dampening factor
742 let direction = loop {
743 // We damp hessian until it is positive definite.
744 // For non-positive definite Hessian, Newton method may give unwanted results.
745 let damped_hessian =
746 &curr_hessian + Tensor::eye(x0_length, (kind, device)) * lambda;
747
748 // Try to perform Banach-Cholesky decomposition of damped hessian
749 match damped_hessian.f_linalg_cholesky(false) {
750 Ok(lower_triangular) => {
751 // Hessian is positive-definite. Solve system with Banach-Cholesky decomposition
752 let y = lower_triangular.linalg_solve_triangular(
753 &negative_grad,
754 false,
755 true,
756 false,
757 );
758 break lower_triangular
759 .transpose(0, 1)
760 .linalg_solve_triangular(&y, true, true, false)
761 .reshape([-1]);
762 }
763 Err(_) => {
764 // Hessian is not positive-definite. Try increasing dampening factor.
765 lambda *= 10.;
766 if lambda > 1e10 {
767 // Dampening factor (lambda) exceeded maximum value. Fallback to pseudoinverse.
768 break curr_hessian
769 .linalg_pinv(1e-14, false)
770 .mm(&negative_grad)
771 .reshape([-1]);
772 }
773 }
774 }
775 };
776
777 // Choose optimal step in given direction using line search
778 let step = choose_step_backtracking(&x, &direction, function, &curr_grad, 0.1, 0.9);
779
780 // Apply step
781 x = x + &step;
782
783 // Check for stop contition
784 let y = function(&x);
785 if let Some(ftol) = self.ftol {
786 if (curr_y.double_value(&[]) - y.double_value(&[])) < ftol {
787 return Ok(x);
788 }
789 }
790 curr_y = y;
791 }
792
793 Ok(x)
794 }
795}
796
797impl fmt::Display for Newton {
798 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
799 let mut string = String::from("Newton(");
800
801 string = string + "max_steps=" + self.max_steps.to_string().as_str();
802 if let Some(gtol) = self.gtol {
803 string = string + ", gtol=" + gtol.to_string().as_str();
804 }
805 if let Some(ftol) = self.ftol {
806 string = string + ", ftol=" + ftol.to_string().as_str();
807 }
808
809 string = string + ")";
810
811 write!(f, "{}", string)
812 }
813}
814
815impl Halley {
816 /// Creates a new Halley optimizer with the given parameters.
817 ///
818 /// # Arguments
819 /// * `max_steps` - Maximum iterations.
820 /// * `gtol` - Optional gradient tolerance.
821 /// * `ftol` - Optional function value change tolerance.
822 ///
823 /// # Returns
824 /// Configured Halley instance.
825 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
826 Self {
827 max_steps,
828 gtol,
829 ftol,
830 }
831 }
832}
833
834impl Optimizer for Halley {
835 fn optimize(
836 &self,
837 function: &dyn Fn(&Tensor) -> Tensor,
838 x0: &Tensor,
839 ) -> anyhow::Result<Tensor> {
840 // Ensure that rank of the initital guess tensor is 1
841 if x0.size().len() != 1 {
842 return Err(anyhow!("`x0` must have rank 1"));
843 }
844
845 // Determine the device and kind for use in the function
846 let kind = x0.kind();
847 let device = x0.device();
848
849 let x0_length = x0.size()[0];
850
851 // Test for sufficient resources for storing tensor of third order derivatives
852 let _ = match Tensor::f_zeros([x0_length, x0_length, x0_length], (kind, device)) {
853 Ok(matrix) => matrix,
854 // Give knowledgable error message to the user
855 // when there is unsufficient memory.
856 Err(tch::TchError::Torch(_)) => {
857 return Err(anyhow!(
858 "Could not allocate {}x{}x{} tensor. Maybe try less resourcefull algorithm.",
859 x0_length,
860 x0_length,
861 x0_length
862 ));
863 }
864 e => e.unwrap(),
865 };
866
867 let mut x = x0.copy();
868 let mut curr_y = function(&x);
869
870 // Ensure that output of `function` is a scalar
871 if curr_y.size() != Vec::<i64>::new() {
872 return Err(anyhow!("Output of function `function` must be scalar"));
873 }
874
875 for _ in 0..self.max_steps {
876 let (curr_grad, curr_hessian, curr_d3_tensor) = derivative_tensors_123(function, &x);
877
878 // Check for stop condition
879 if let Some(gtol) = self.gtol {
880 if curr_grad.norm().double_value(&[]) < gtol {
881 return Ok(x);
882 }
883 } else {
884 // This check is necessary. Continuation of the algorithm
885 // with gradient equal to exactly zero leads to NaN appearing
886 // in the result.
887 if curr_grad.norm().double_value(&[]) == 0. {
888 return Ok(x);
889 }
890 }
891
892 // Calculate step direction
893 let hessian_pinv = curr_hessian.linalg_pinv(1e-14, false);
894 let neg_newton_dir = hessian_pinv.mm(&curr_grad.reshape([-1, 1]));
895 let direction = -hessian_pinv.mm(&(curr_grad.reshape([-1, 1]) + curr_d3_tensor.matmul(&neg_newton_dir).reshape([x0_length, x0_length]).mm(&neg_newton_dir)*0.5)).reshape([-1]);
896
897 // Choose optimal step in given direction using line search
898 let step = choose_step_backtracking(&x, &direction, function, &curr_grad, 0.1, 0.9);
899
900 // Apply step
901 x = x + &step;
902
903 // Check for stop contition
904 let y = function(&x);
905 if let Some(ftol) = self.ftol {
906 if (curr_y.double_value(&[]) - y.double_value(&[])) < ftol {
907 return Ok(x);
908 }
909 }
910 curr_y = y;
911 }
912
913 Ok(x)
914 }
915}
916
917impl fmt::Display for Halley {
918 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
919 let mut string = String::from("Halley(");
920
921 string = string + "max_steps=" + self.max_steps.to_string().as_str();
922 if let Some(gtol) = self.gtol {
923 string = string + ", gtol=" + gtol.to_string().as_str();
924 }
925 if let Some(ftol) = self.ftol {
926 string = string + ", ftol=" + ftol.to_string().as_str();
927 }
928
929 string = string + ")";
930
931 write!(f, "{}", string)
932 }
933}