Skip to main content

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}