1use anyhow::anyhow;
6use std::fmt;
7use tch::{IndexOp, Tensor};
8
9#[cfg(feature = "warnings")]
10use tracing::warn;
11
12#[cfg(not(feature = "warnings"))]
13macro_rules! warn {
14 ($($arg:tt)*) => {};
15}
16
17pub trait Optimizer: Send + Sync + fmt::Display {
19 fn optimize(&self, function: &dyn Fn(&Tensor) -> Tensor, x0: &Tensor)
32 -> anyhow::Result<Tensor>;
33}
34
35fn validate_optimizer_output(tensor: &Tensor, optimizer_name: &str) -> anyhow::Result<()> {
37 if tensor.isfinite().f_all()?.f_int64_value(&[])? == 0 {
38 anyhow::bail!(
39 "Optimizer {} produced non-finite result (NaN/Inf)",
40 optimizer_name
41 );
42 }
43 Ok(())
44}
45
46pub struct Newton {
56 max_steps: usize,
58 gtol: Option<f64>,
60 ftol: Option<f64>,
62}
63
64pub struct BFGS {
75 max_steps: usize,
77 gtol: Option<f64>,
79 ftol: Option<f64>,
81}
82
83pub struct Halley {
93 max_steps: usize,
95 gtol: Option<f64>,
97 ftol: Option<f64>,
99}
100
101pub struct CG {
112 max_steps: usize,
114 gtol: Option<f64>,
116 ftol: Option<f64>,
118}
119
120pub(crate) fn differentiate(
129 function: &dyn Fn(&Tensor) -> Tensor,
130 x: &Tensor,
131) -> anyhow::Result<Tensor> {
132 let x_with_grad = x.f_detach()?.copy().set_requires_grad(true);
133 let y = function(&x_with_grad);
134
135 if y.size() != [] as [i64; 0] {
136 return Err(anyhow!(
137 "Bad shape of `y`. Expected [], but got {:?}",
138 y.size()
139 ));
140 }
141
142 if !y.requires_grad() {
143 return Ok(tch::Tensor::f_zeros(x.size(), (x.kind(), x.device()))?);
144 }
145
146 let gradient = tch::Tensor::f_run_backward(&[y], &[x_with_grad], false, false)?[0].copy();
147
148 Ok(gradient)
152}
153
154pub(crate) fn gradient_and_hessian(
162 function: &dyn Fn(&Tensor) -> Tensor,
163 x: &Tensor,
164) -> anyhow::Result<(Tensor, Tensor)> {
165 let x_with_grad = x.f_detach()?.copy().set_requires_grad(true);
166 let y = function(&x_with_grad);
167
168 if y.size() != [] as [i64; 0] {
169 return Err(anyhow!(
170 "Bad shape of `y`. Expected [], but got {:?}",
171 y.size()
172 ));
173 }
174
175 if !y.requires_grad() {
176 return Ok((
177 tch::Tensor::f_zeros(x.size(), (x.kind(), x.device()))?,
178 tch::Tensor::f_zeros([x.size()[0], x.size()[0]], (x.kind(), x.device()))?,
179 ));
180 }
181
182 let grad = Tensor::f_run_backward(&[y], &[&x_with_grad], true, true)?[0].copy();
185 let grad_len = grad.size()[0];
186 let grad_kind = grad.kind();
187 let grad_device = grad.device();
188
189 if !grad.requires_grad() {
192 return Ok((
193 grad,
194 Tensor::f_zeros([grad_len, grad_len], (grad_kind, grad_device))?,
195 ));
196 }
197
198 let mut vectors = Vec::<Tensor>::with_capacity(grad_len as usize);
199 for i in 0..grad_len {
200 vectors.append(&mut Tensor::f_run_backward(
203 &[grad.i(i)],
204 &[&x_with_grad],
205 true,
206 false,
207 )?);
208 }
209
210 let grad = grad.f_detach()?;
212 let hessian = Tensor::f_stack(&vectors, 0)?.f_detach()?;
214
215 Ok((grad, hessian))
216}
217
218pub(crate) fn derivative_tensors_123(
226 function: &dyn Fn(&Tensor) -> Tensor,
227 x: &Tensor,
228) -> anyhow::Result<(Tensor, Tensor, Tensor)> {
229 let x_with_grad = x.f_detach()?.copy().set_requires_grad(true);
230 let y = function(&x_with_grad);
231
232 if y.size() != [] as [i64; 0] {
233 return Err(anyhow!(
234 "Bad shape of `y`. Expected [], but got {:?}",
235 y.size()
236 ));
237 }
238
239 if !y.requires_grad() {
240 return Ok((
241 tch::Tensor::f_zeros(x.size(), (x.kind(), x.device()))?,
242 tch::Tensor::f_zeros([x.size()[0], x.size()[0]], (x.kind(), x.device()))?,
243 tch::Tensor::f_zeros(
244 [x.size()[0], x.size()[0], x.size()[0]],
245 (x.kind(), x.device()),
246 )?,
247 ));
248 }
249
250 let grad = Tensor::f_run_backward(&[y], &[&x_with_grad], true, true)?[0].copy();
253 let grad_len = grad.size()[0];
254 let grad_kind = grad.kind();
255 let grad_device = grad.device();
256
257 if !grad.requires_grad() {
261 return Ok((
262 grad,
263 Tensor::f_zeros([grad_len, grad_len], (grad_kind, grad_device))?,
264 Tensor::f_zeros([grad_len, grad_len, grad_len], (grad_kind, grad_device))?,
265 ));
266 }
267
268 let mut vectors = Vec::<Tensor>::with_capacity(grad_len as usize);
269 for i in 0..grad_len {
270 vectors.append(&mut Tensor::f_run_backward(
273 &[grad.i(i)],
274 &[&x_with_grad],
275 true,
276 true,
277 )?);
278 }
279
280 let hessian = Tensor::f_stack(&vectors, 0)?;
282
283 if !hessian.requires_grad() {
287 return Ok((
288 grad,
289 hessian,
290 Tensor::f_zeros([grad_len, grad_len, grad_len], (grad_kind, grad_device))?,
291 ));
292 }
293
294 let mut vectors2 = Vec::<Tensor>::with_capacity(grad_len as usize);
295 for i in 0..grad_len {
296 let mut vectors1 = Vec::<Tensor>::with_capacity(grad_len as usize);
297 for j in 0..grad_len {
298 vectors1.append(&mut Tensor::f_run_backward(
299 &[hessian.i((i, j))],
300 &[&x_with_grad],
301 true,
302 false,
303 )?);
304 }
305 vectors2.push(Tensor::f_stack(&vectors1, 0)?);
306 }
307
308 let grad = grad.f_detach()?;
310 let hessian = hessian.f_detach()?;
311 let d3_tensor = Tensor::f_stack(&vectors2, 0)?.f_detach()?;
313
314 Ok((grad, hessian, d3_tensor))
315}
316
317const P0: f64 = 0.0000000001f64;
319
320const PHI2: f64 = 2.618033988749894848207f64;
322const RPHI: f64 = 0.618033988749894848207f64;
324
325fn choose_step_golden_section(
337 x0: &Tensor,
338 direction: &Tensor,
339 function: &dyn Fn(&Tensor) -> Tensor,
340 atol: f64,
341) -> anyhow::Result<Tensor> {
342 let (mut x1, mut x2, mut x3, mut x4): (f64, f64, f64, f64);
343 let (fx1, mut fx3, mut fx4): (f64, f64, f64);
344
345 fx1 = function(&x0).f_double_value(&[])?;
346
347 x1 = 0.;
348 let fx_guess = function(&(x0 + direction * atol * 15.)).f_double_value(&[])?;
351 x2 = if !fx_guess.is_finite() || fx_guess > fx1 {
352 P0
353 } else {
354 atol * 15.
355 };
356 let mut fx = function(&(x0 + direction * x2)).f_double_value(&[])?;
358 let mut forward_iters: u32 = 0;
359 while fx <= fx1 {
360 let new_x2 = x1 + (x2 - x1) * PHI2;
361 fx = function(&(x0 + direction * new_x2)).f_double_value(&[])?;
362 if !fx.is_finite() {
363 break;
364 }
365 x2 = new_x2;
366 forward_iters += 1;
367 }
368
369 x3 = x2 - (x2 - x1) * RPHI;
370 x4 = x1 + (x2 - x1) * RPHI;
371 fx3 = function(&(x0 + direction * x3)).f_double_value(&[])?;
372 fx4 = function(&(x0 + direction * x4)).f_double_value(&[])?;
373
374 let mut refine_iters: u32 = 0;
375 while x2 - x1 > atol && refine_iters < 500 {
376 if fx3 < fx4 {
377 x2 = x4;
378
379 fx4 = fx3;
380 x3 = x2 - (x2 - x1) * RPHI;
381 x4 = x1 + (x2 - x1) * RPHI;
382 fx3 = function(&(x0 + direction * x3)).f_double_value(&[])?;
383 } else {
384 x1 = x3;
385
386 fx3 = fx4;
387 x3 = x2 - (x2 - x1) * RPHI;
388 x4 = x1 + (x2 - x1) * RPHI;
389 fx4 = function(&(x0 + direction * x4)).f_double_value(&[])?;
390 }
391 refine_iters += 1;
392 }
393
394 if forward_iters >= 100 {
396 warn!(
397 "golden section line search: forward search took {} iterations without bracketing a minimum; direction may be poor",
398 forward_iters
399 );
400 }
401
402 if refine_iters >= 200 {
403 warn!(
404 "golden section line search: refinement took {} iterations; atol may be too small or objective flat",
405 refine_iters
406 );
407 }
408
409 Ok(direction * ((x1 + x2) / 2.))
410}
411
412fn choose_step_backtracking(
425 x0: &Tensor,
426 direction: &Tensor,
427 function: &dyn Fn(&Tensor) -> Tensor,
428 grad: &Tensor,
429 alpha: f64,
430 beta: f64,
431) -> anyhow::Result<Tensor> {
432 let fx0 = function(&x0).f_double_value(&[])?;
433
434 let mut t = 1f64;
435 let mut backtrack_iters: u32 = 0;
436
437 while {
438 let fx = function(&(x0 + direction * t)).f_double_value(&[])?;
439
440 if !fx.is_finite() {
441 true
442 } else {
443 fx > fx0
444 + grad
445 .f_reshape([-1])?
446 .f_dot(&direction.f_reshape([-1])?)?
447 .f_double_value(&[])?
448 * alpha
449 * t
450 }
451 } {
452 t *= beta;
453 backtrack_iters += 1;
454 if t < 1e-30 {
455 break;
456 }
457 }
458
459 if backtrack_iters >= 100 {
461 warn!(
462 "backtracking line search: {} iterations without satisfying Armijo condition; direction may not be a descent direction",
463 backtrack_iters
464 );
465 }
466
467 if t < 1e-30 {
468 warn!(
469 "backtracking line search: step size collapsed to {:.3e}; optimizer may be stuck",
470 t
471 );
472 }
473
474 Ok(direction.copy() * t)
475}
476
477impl CG {
478 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
479 Self {
480 max_steps,
481 gtol,
482 ftol,
483 }
484 }
485}
486
487impl Optimizer for CG {
488 fn optimize(
498 &self,
499 function: &dyn Fn(&Tensor) -> Tensor,
500 x0: &Tensor,
501 ) -> anyhow::Result<Tensor> {
502 if x0.size().len() != 1 {
504 return Err(anyhow!("`x0` must have rank 1"));
505 }
506
507 let mut prev3_step_norm = 0f64;
508 let mut prev2_step_norm = 0f64;
509 let mut prev_step_norm = 0f64;
510
511 let mut prev_grad = Tensor::f_zeros_like(&x0)?;
512 let mut prev_direction = Tensor::f_zeros_like(&x0)?;
513 let mut prev_y: Option<Tensor> = None;
514 let mut x = x0.copy();
515
516 let mut warned_nonfinite_grad = false;
517 let mut warned_beta_clamp = false;
518 let mut warned_nonfinite_iter = false;
519
520 for step_num in 0..self.max_steps {
521 let grad = match differentiate(function, &x) {
522 Ok(grad) => grad,
523 Err(e) => {
524 return Err(anyhow!(
525 "Runtime error: Differentiation failed in CG optimizer: {}",
526 e
527 ));
528 }
529 };
530
531 if !warned_nonfinite_grad && grad.isfinite().f_all()?.f_int64_value(&[])? == 0 {
533 warn!("CG: non-finite gradient detected; function may be ill-defined");
534 warned_nonfinite_grad = true;
535 }
536
537 if let Some(gtol) = self.gtol {
539 if grad.norm().f_double_value(&[])? < gtol {
540 validate_optimizer_output(&x, "CG")?;
542 return Ok(x);
543 }
544 } else {
545 if grad.norm().f_double_value(&[])? == 0. {
549 validate_optimizer_output(&x, "CG")?;
551 return Ok(x);
552 }
553 }
554
555 let direction = match step_num {
557 0 => -&grad,
558 _ => {
559 let orthogonality_measure = grad
560 .f_reshape([-1])?
561 .f_dot(&prev_grad.f_reshape([-1])?)?
562 .f_abs()?
563 / grad.f_reshape([-1])?.f_dot(&grad.f_reshape([-1])?)?;
564 if orthogonality_measure.f_double_value(&[])? > 0.2 {
565 -&grad
567 } else {
568 let beta = grad
569 .f_reshape([-1])?
570 .f_dot(&(&grad - &prev_grad).f_reshape([-1])?)?
571 / prev_grad
572 .f_reshape([-1])?
573 .f_dot(&prev_grad.f_reshape([-1])?)?;
574 let beta = if beta.f_double_value(&[])? > 0. {
576 beta
577 } else {
578 tch::Tensor::f_zeros_like(&beta)?
579 };
580 let beta = if beta.f_double_value(&[])? > 1e12 {
582 if !warned_beta_clamp {
583 warn!("CG: beta clamped to 1e12; optimizer may be diverging");
584 warned_beta_clamp = true;
585 }
586 tch::Tensor::f_ones_like(&beta)? * 1e12
587 } else {
588 beta
589 };
590
591 -&grad + beta * &prev_direction
592 }
593 }
594 };
595
596 let linesearch_atol =
598 P0.max(prev_step_norm.min(prev2_step_norm).min(prev3_step_norm) / 1000.);
599
600 let step = choose_step_golden_section(&x, &direction, &function, linesearch_atol)?;
603
604 prev3_step_norm = prev2_step_norm;
606 prev2_step_norm = prev_step_norm;
607 prev_step_norm = step.f_norm()?.f_double_value(&[])?;
608
609 x = x + step;
611
612 if !warned_nonfinite_iter && x.isfinite().f_all()?.f_int64_value(&[])? == 0 {
614 warn!("CG: non-finite iterate detected; step size may be too large");
615 warned_nonfinite_iter = true;
616 }
617
618 let y = function(&x);
620 if let (Some(prev_y), Some(ftol)) = (prev_y, self.ftol) {
621 if (&prev_y - &y).f_double_value(&[])? < ftol {
622 validate_optimizer_output(&x, "CG")?;
624 return Ok(x);
625 }
626 }
627 prev_y = Some(y);
628
629 prev_grad = grad;
631 prev_direction = direction;
632 }
633
634 validate_optimizer_output(&x, "CG")?;
636 Ok(x)
637 }
638}
639
640impl fmt::Display for CG {
641 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
642 let mut string = String::from("CG(");
643
644 string = string + "max_steps=" + self.max_steps.to_string().as_str();
645 if let Some(gtol) = self.gtol {
646 string = string + ", gtol=" + gtol.to_string().as_str();
647 }
648 if let Some(ftol) = self.ftol {
649 string = string + ", ftol=" + ftol.to_string().as_str();
650 }
651 string = string + ")";
652
653 write!(f, "{}", string)
654 }
655}
656
657impl BFGS {
658 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
668 Self {
669 max_steps,
670 gtol,
671 ftol,
672 }
673 }
674}
675
676impl Optimizer for BFGS {
677 fn optimize(
678 &self,
679 function: &dyn Fn(&Tensor) -> Tensor,
680 x0: &Tensor,
681 ) -> anyhow::Result<Tensor> {
682 if x0.size().len() != 1 {
684 return Err(anyhow!("`x0` must have rank 1"));
685 }
686
687 let kind = x0.kind();
689 let device = x0.device();
690
691 let mut prev3_step_norm = 0f64;
692 let mut prev2_step_norm = 0f64;
693 let mut prev_step_norm = 0f64;
694
695 let x0_length = x0.size()[0];
696 let identity = match Tensor::f_eye(x0_length, (kind, device)) {
697 Ok(matrix) => matrix,
698 Err(tch::TchError::Torch(_)) => {
702 return Err(anyhow!(
703 "Could not allocate {}x{} matrix. Maybe try less resourcefull algorithm.",
704 x0_length,
705 x0_length
706 ));
707 }
708 e => e.unwrap(),
709 };
710 let mut x = x0.copy();
711 let mut appr_inv_h = identity.copy();
712 let mut curr_grad = match differentiate(function, &x) {
713 Ok(grad) => grad,
714 Err(e) => {
715 return Err(anyhow!(
716 "Runtime error: Differentiation failed in BFGS optimizer: {}",
717 e
718 ));
719 }
720 };
721 let mut curr_y = function(&x);
722
723 if curr_y.size() != Vec::<i64>::new() {
725 return Err(anyhow!("Output of function `function` must be scalar"));
726 }
727
728 let mut warned_inv_hess_large = false;
729
730 for _ in 0..self.max_steps {
731 if let Some(gtol) = self.gtol {
733 if curr_grad.f_norm()?.f_double_value(&[])? < gtol {
734 validate_optimizer_output(&x, "BFGS")?;
736 return Ok(x);
737 }
738 } else {
739 if curr_grad.f_norm()?.f_double_value(&[])? == 0. {
743 validate_optimizer_output(&x, "BFGS")?;
745 return Ok(x);
746 }
747 }
748
749 let direction = (-appr_inv_h.f_mm(&curr_grad.f_reshape([-1, 1])?)?).f_reshape([-1])?;
751
752 let linesearch_atol =
754 P0.max(prev_step_norm.min(prev2_step_norm).min(prev3_step_norm) / 100.);
755
756 let step = choose_step_golden_section(&x, &direction, function, linesearch_atol)?;
759
760 prev3_step_norm = prev2_step_norm;
762 prev2_step_norm = prev_step_norm;
763 prev_step_norm = step.f_norm()?.f_double_value(&[])?;
764
765 x = x + &step;
767
768 let y = function(&x);
770 if let Some(ftol) = self.ftol {
771 if (curr_y.f_double_value(&[])? - y.f_double_value(&[])?) < ftol {
772 validate_optimizer_output(&x, "BFGS")?;
774 return Ok(x);
775 }
776 }
777 curr_y = y;
778
779 let grad = match differentiate(function, &x) {
780 Ok(grad) => grad,
781 Err(e) => {
782 return Err(anyhow!(
783 "Runtime error: Differentiation failed in BFGS optimizer: {}",
784 e
785 ));
786 }
787 };
788 let gdiff = &grad - &curr_grad;
789
790 let gamma = {
793 let delta = 0.0001;
794
795 let sty = step.f_dot(&gdiff)?.f_double_value(&[])?;
796 let step_norm_sq = step.f_dot(&step)?.f_double_value(&[])?;
797
798 let theta = if sty >= delta * step_norm_sq {
799 1.
800 } else {
801 let numerator = (1. - delta) * step_norm_sq;
802 let denominator = step_norm_sq - sty;
803
804 if denominator.abs() < 1e-10 {
805 1.
806 } else {
807 (numerator / denominator).min(1.)
808 }
809 };
810
811 let projection_factor = if step_norm_sq < 1e-10 {
812 0.
813 } else {
814 sty / step_norm_sq
815 };
816 let gdiff_prime = &gdiff * theta + &step * ((1. - theta) * projection_factor);
817 let sty_prime = step.f_dot(&gdiff_prime)?.f_double_value(&[])?;
818
819 if sty_prime.abs() < 1e-10 {
820 1. / (delta * step_norm_sq + 1e-10)
821 } else {
822 1. / sty_prime
823 }
824 };
825
826 appr_inv_h = (&identity
828 - gamma * step.f_reshape([-1, 1])?.f_mm(&gdiff.f_reshape([1, -1])?)?)
829 .f_mm(&appr_inv_h)?
830 .f_mm(
831 &(&identity - gamma * gdiff.f_reshape([-1, 1])?.f_mm(&step.f_reshape([1, -1])?)?),
832 )? + gamma * step.f_reshape([-1, 1])?.f_mm(&step.f_reshape([1, -1])?)?;
833
834 let inv_h_norm = appr_inv_h.f_norm()?.f_double_value(&[])?;
836 if !warned_inv_hess_large && inv_h_norm > 1e10 {
837 warn!(
838 "BFGS: inverse Hessian approximation norm reached {:.3e}; problem may be ill-conditioned",
839 inv_h_norm
840 );
841 warned_inv_hess_large = true;
842 }
843
844 curr_grad = grad;
845 }
846
847 validate_optimizer_output(&x, "BFGS")?;
849 Ok(x)
850 }
851}
852
853impl fmt::Display for BFGS {
854 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
855 let mut string = String::from("BFGS(");
856
857 string = string + "max_steps=" + self.max_steps.to_string().as_str();
858 if let Some(gtol) = self.gtol {
859 string = string + ", gtol=" + gtol.to_string().as_str();
860 }
861 if let Some(ftol) = self.ftol {
862 string = string + ", ftol=" + ftol.to_string().as_str();
863 }
864
865 string = string + ")";
866
867 write!(f, "{}", string)
868 }
869}
870
871impl Newton {
872 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
882 Self {
883 max_steps,
884 gtol,
885 ftol,
886 }
887 }
888}
889
890impl Optimizer for Newton {
891 fn optimize(
892 &self,
893 function: &dyn Fn(&Tensor) -> Tensor,
894 x0: &Tensor,
895 ) -> anyhow::Result<Tensor> {
896 if x0.size().len() != 1 {
898 return Err(anyhow!("`x0` must have rank 1"));
899 }
900
901 let kind = x0.kind();
903 let device = x0.device();
904
905 let x0_length = x0.size()[0];
906
907 let _ = match Tensor::f_eye(x0_length, (kind, device)) {
909 Ok(matrix) => matrix,
910 Err(tch::TchError::Torch(_)) => {
913 return Err(anyhow!(
914 "Could not allocate {}x{} matrix. Maybe try less resourcefull algorithm.",
915 x0_length,
916 x0_length
917 ));
918 }
919 e => e.unwrap(),
920 };
921
922 let mut x = x0.copy();
923 let mut curr_y = function(&x);
924
925 if curr_y.size() != Vec::<i64>::new() {
927 return Err(anyhow!("Output of function `function` must be scalar"));
928 }
929
930 let mut warned_damping_moderate = false;
931 let mut warned_damping_severe = false;
932
933 for _ in 0..self.max_steps {
934 let (curr_grad, curr_hessian) = match gradient_and_hessian(function, &x) {
935 Ok(gh) => gh,
936 Err(e) => {
937 return Err(anyhow!(
938 "Runtime error: Differentiation failed in Newton optimizer: {}",
939 e
940 ));
941 }
942 };
943
944 if let Some(gtol) = self.gtol {
946 if curr_grad.f_norm()?.f_double_value(&[])? < gtol {
947 validate_optimizer_output(&x, "Newton")?;
949 return Ok(x);
950 }
951 } else {
952 if curr_grad.f_norm()?.f_double_value(&[])? == 0. {
956 validate_optimizer_output(&x, "Newton")?;
958 return Ok(x);
959 }
960 }
961
962 let negative_grad = -curr_grad.f_reshape([-1, 1])?; let mut lambda = (negative_grad.f_norm()?.f_double_value(&[])? * 1e-3).max(1e-8); let direction = loop {
966 let damped_hessian =
969 &curr_hessian + Tensor::f_eye(x0_length, (kind, device))? * lambda;
970
971 match damped_hessian.f_linalg_cholesky(false) {
973 Ok(lower_triangular) => {
974 let y = lower_triangular.f_linalg_solve_triangular(
976 &negative_grad,
977 false,
978 true,
979 false,
980 )?;
981 break lower_triangular
982 .f_transpose(0, 1)?
983 .f_linalg_solve_triangular(&y, true, true, false)?
984 .reshape([-1]);
985 }
986 Err(_) => {
987 lambda *= 10.;
989
990 if !warned_damping_moderate && lambda >= 1e3 && lambda < 1e7 {
992 warn!(
993 "Newton: Hessian required damping factor {:.3e}; problem may be ill-conditioned",
994 lambda
995 );
996 warned_damping_moderate = true;
997 }
998
999 if !warned_damping_severe && lambda >= 1e10 {
1000 warn!(
1001 "Newton: Hessian damping factor reached {:.3e}; falling back to pseudoinverse (Hessian is severely ill-conditioned)",
1002 lambda
1003 );
1004 warned_damping_severe = true;
1005 }
1006
1007 if lambda > 1e10 {
1008 break curr_hessian
1010 .f_linalg_pinv(1e-14, false)?
1011 .f_mm(&negative_grad)?
1012 .f_reshape([-1])?;
1013 }
1014 }
1015 }
1016 };
1017
1018 let step = choose_step_backtracking(&x, &direction, function, &curr_grad, 0.1, 0.9)?;
1021
1022 x = x + &step;
1024
1025 let y = function(&x);
1027 if let Some(ftol) = self.ftol {
1028 if (curr_y.f_double_value(&[])? - y.f_double_value(&[])?) < ftol {
1029 validate_optimizer_output(&x, "Newton")?;
1031 return Ok(x);
1032 }
1033 }
1034 curr_y = y;
1035 }
1036
1037 validate_optimizer_output(&x, "Newton")?;
1039 Ok(x)
1040 }
1041}
1042
1043impl fmt::Display for Newton {
1044 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1045 let mut string = String::from("Newton(");
1046
1047 string = string + "max_steps=" + self.max_steps.to_string().as_str();
1048 if let Some(gtol) = self.gtol {
1049 string = string + ", gtol=" + gtol.to_string().as_str();
1050 }
1051 if let Some(ftol) = self.ftol {
1052 string = string + ", ftol=" + ftol.to_string().as_str();
1053 }
1054
1055 string = string + ")";
1056
1057 write!(f, "{}", string)
1058 }
1059}
1060
1061impl Halley {
1062 pub fn new(max_steps: usize, gtol: Option<f64>, ftol: Option<f64>) -> Self {
1072 Self {
1073 max_steps,
1074 gtol,
1075 ftol,
1076 }
1077 }
1078}
1079
1080impl Optimizer for Halley {
1081 fn optimize(
1082 &self,
1083 function: &dyn Fn(&Tensor) -> Tensor,
1084 x0: &Tensor,
1085 ) -> anyhow::Result<Tensor> {
1086 if x0.size().len() != 1 {
1088 return Err(anyhow!("`x0` must have rank 1"));
1089 }
1090
1091 let kind = x0.kind();
1093 let device = x0.device();
1094
1095 let x0_length = x0.size()[0];
1096
1097 let _ = match Tensor::f_zeros([x0_length, x0_length, x0_length], (kind, device)) {
1099 Ok(matrix) => matrix,
1100 Err(tch::TchError::Torch(_)) => {
1103 return Err(anyhow!(
1104 "Could not allocate {}x{}x{} tensor. Maybe try less resourcefull algorithm.",
1105 x0_length,
1106 x0_length,
1107 x0_length
1108 ));
1109 }
1110 e => e.unwrap(),
1111 };
1112
1113 let mut x = x0.copy();
1114 let mut curr_y = function(&x);
1115
1116 if curr_y.size() != Vec::<i64>::new() {
1118 return Err(anyhow!("Output of function `function` must be scalar"));
1119 }
1120
1121 let mut warned_pinv_large = false;
1122
1123 for _ in 0..self.max_steps {
1124 let (curr_grad, curr_hessian, curr_d3_tensor) =
1125 match derivative_tensors_123(function, &x) {
1126 Ok(ghd3) => ghd3,
1127 Err(e) => {
1128 return Err(anyhow!(
1129 "Runtime error: Differentiation failed in Halley optimizer: {}",
1130 e
1131 ));
1132 }
1133 };
1134
1135 if let Some(gtol) = self.gtol {
1137 if curr_grad.f_norm()?.f_double_value(&[])? < gtol {
1138 validate_optimizer_output(&x, "Halley")?;
1140 return Ok(x);
1141 }
1142 } else {
1143 if curr_grad.f_norm()?.f_double_value(&[])? == 0. {
1147 validate_optimizer_output(&x, "Halley")?;
1149 return Ok(x);
1150 }
1151 }
1152
1153 let hessian_pinv = curr_hessian.f_linalg_pinv(1e-14, false)?;
1155
1156 let pinv_norm = hessian_pinv.f_norm()?.f_double_value(&[])?;
1158 if !warned_pinv_large && pinv_norm > 1e8 {
1159 warn!(
1160 "Halley: Hessian pseudoinverse norm is {:.3e}; Hessian may be ill-conditioned",
1161 pinv_norm
1162 );
1163 warned_pinv_large = true;
1164 }
1165
1166 let neg_newton_dir = hessian_pinv.f_mm(&curr_grad.f_reshape([-1, 1])?)?;
1167 let direction = -hessian_pinv
1168 .f_mm(
1169 &(curr_grad.f_reshape([-1, 1])?
1170 + curr_d3_tensor
1171 .f_matmul(&neg_newton_dir)?
1172 .f_reshape([x0_length, x0_length])?
1173 .f_mm(&neg_newton_dir)?
1174 * 0.5),
1175 )?
1176 .f_reshape([-1])?;
1177
1178 let step = choose_step_backtracking(&x, &direction, function, &curr_grad, 0.1, 0.9)?;
1181
1182 x = x + &step;
1184
1185 let y = function(&x);
1187 if let Some(ftol) = self.ftol {
1188 if (curr_y.f_double_value(&[])? - y.f_double_value(&[])?) < ftol {
1189 validate_optimizer_output(&x, "Halley")?;
1191 return Ok(x);
1192 }
1193 }
1194 curr_y = y;
1195 }
1196
1197 validate_optimizer_output(&x, "Halley")?;
1199 Ok(x)
1200 }
1201}
1202
1203impl fmt::Display for Halley {
1204 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1205 let mut string = String::from("Halley(");
1206
1207 string = string + "max_steps=" + self.max_steps.to_string().as_str();
1208 if let Some(gtol) = self.gtol {
1209 string = string + ", gtol=" + gtol.to_string().as_str();
1210 }
1211 if let Some(ftol) = self.ftol {
1212 string = string + ", ftol=" + ftol.to_string().as_str();
1213 }
1214
1215 string = string + ")";
1216
1217 write!(f, "{}", string)
1218 }
1219}