1use std::fmt::Debug;
2use scirs2_core::ndarray::{Array1, Array2, Array3};
9use scirs2_core::numeric::Float;
10
11use crate::error::{OptimError, Result};
12
13pub mod hvp;
17
18pub struct HigherOrderEngine<
20 T: Float
21 + Debug
22 + Default
23 + Clone
24 + Send
25 + Sync
26 + 'static
27 + std::iter::Sum
28 + scirs2_core::ndarray::ScalarOperand,
29> {
30 _maxorder: usize,
32
33 mixed_mode: bool,
35
36 finite_diff_eps: T,
38
39 parallel_computation: bool,
41
42 thread_pool_size: usize,
44
45 adaptive_sparsity: bool,
47
48 auto_mode_selection: bool,
50
51 profiler: ComputationProfiler<T>,
53}
54
55#[derive(Debug, Clone)]
57pub struct HessianConfig {
58 pub exact: bool,
60
61 pub sparse: bool,
63
64 pub sparsity_threshold: f64,
66
67 pub diagonal_only: bool,
69
70 pub bfgs_approximation: bool,
72
73 pub verify_with_finite_diff: bool,
75}
76
77impl Default for HessianConfig {
78 fn default() -> Self {
79 Self {
80 exact: true,
81 sparse: false,
82 sparsity_threshold: 1e-8,
83 diagonal_only: false,
84 bfgs_approximation: false,
85 verify_with_finite_diff: false,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct SparseHessian<T: Float + Debug + Send + Sync + 'static> {
93 pub rows: Vec<usize>,
95
96 pub cols: Vec<usize>,
98
99 pub values: Vec<T>,
101
102 pub shape: (usize, usize),
104
105 pub nnz: usize,
107}
108
109#[derive(Debug, Clone)]
111pub struct ThirdOrderTensor<T: Float + Debug + Send + Sync + 'static> {
112 pub data: Array3<T>,
114
115 pub shape: (usize, usize, usize),
117}
118
119#[derive(Debug, Clone)]
121pub struct MixedPartials<T: Float + Debug + Send + Sync + 'static> {
122 pub variables: Vec<usize>,
124
125 pub orders: Vec<usize>,
127
128 pub value: T,
130
131 pub method: MixedPartialMethod,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum MixedPartialMethod {
145 FiniteDifference,
148
149 ForwardFiniteDifference,
153
154 NestedAutodiff,
160}
161
162#[derive(Debug, Clone)]
164pub struct LayerInfo<T: Float + Debug + Send + Sync + 'static> {
165 pub layer_type: LayerType,
166 pub input_size: usize,
167 pub output_size: usize,
168 pub weights: Array2<T>,
169 pub bias: Option<Array1<T>>,
170}
171
172#[derive(Debug, Clone, Copy)]
174pub enum LayerType {
175 Linear,
176 Convolutional,
177 LSTM,
178 Attention,
179}
180
181fn central_step<T: Float>(order: usize) -> T {
191 let eps_mach = T::epsilon();
192 let denom = T::from(order + 2).unwrap_or_else(|| T::from(3usize).unwrap_or_else(T::one));
193 eps_mach.powf(T::one() / denom)
194}
195
196impl<
197 T: Float
198 + Debug
199 + Default
200 + Clone
201 + Send
202 + Sync
203 + 'static
204 + std::iter::Sum
205 + scirs2_core::ndarray::ScalarOperand,
206 > HigherOrderEngine<T>
207{
208 pub fn new(_maxorder: usize) -> Self {
210 Self {
211 _maxorder,
212 mixed_mode: true,
213 finite_diff_eps: scirs2_core::numeric::NumCast::from(1e-5).unwrap_or_else(|| T::zero()),
214 parallel_computation: true,
215 thread_pool_size: 4, adaptive_sparsity: true,
217 auto_mode_selection: true,
218 profiler: ComputationProfiler::new(),
219 }
220 }
221
222 pub fn with_config(config: HigherOrderConfig<T>) -> Self {
224 Self {
225 _maxorder: config._maxorder,
226 mixed_mode: config.mixed_mode,
227 finite_diff_eps: config.finite_diff_eps,
228 parallel_computation: config.parallel_computation,
229 thread_pool_size: config.thread_pool_size,
230 adaptive_sparsity: config.adaptive_sparsity,
231 auto_mode_selection: config.auto_mode_selection,
232 profiler: ComputationProfiler::new(),
233 }
234 }
235
236 pub fn set_mixed_mode(&mut self, enabled: bool) {
238 self.mixed_mode = enabled;
239 }
240
241 pub fn set_finite_diff_eps(&mut self, eps: T) {
243 self.finite_diff_eps = eps;
244 }
245
246 fn fd_step(&self, order: usize) -> T {
253 let machine = central_step::<T>(order);
254 if self.finite_diff_eps > machine {
255 self.finite_diff_eps
256 } else {
257 machine
258 }
259 }
260
261 pub fn hessian_forward_over_reverse(
274 &mut self,
275 function: impl Fn(&Array1<T>) -> T,
276 point: &Array1<T>,
277 config: &HessianConfig,
278 ) -> Result<Array2<T>> {
279 let n = point.len();
280
281 if config.diagonal_only {
282 return self.hessian_diagonal(&function, point);
283 }
284
285 let started = std::time::Instant::now();
286 let parallel = self.should_use_parallel(n);
287 let sparse = self.should_use_sparse(n, config);
288 let mut hessian = Array2::zeros((n, n));
289
290 for i in 0..n {
292 let grad_fn = |x: &Array1<T>| -> Array1<T> {
293 self.gradient_at_point(&function, x)
294 .unwrap_or_else(|_| Array1::zeros(n))
295 };
296
297 let mut direction = Array1::zeros(n);
299 direction[i] = T::one();
300
301 let hessian_row =
302 self.directional_derivative_of_gradient(&grad_fn, point, &direction)?;
303
304 for j in 0..n {
305 hessian[[i, j]] = hessian_row[j];
306 }
307 }
308
309 if config.exact {
311 for i in 0..n {
312 for j in i + 1..n {
313 let avg = (hessian[[i, j]] + hessian[[j, i]])
314 / scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero());
315 hessian[[i, j]] = avg;
316 hessian[[j, i]] = avg;
317 }
318 }
319 }
320
321 if config.verify_with_finite_diff {
323 let fd_hessian = self.finite_difference_hessian(&function, point)?;
324 self.verify_hessian_accuracy(&hessian, &fd_hessian)?;
325 }
326
327 if sparse {
328 hessian = self.apply_adaptive_sparsity(hessian, config.sparsity_threshold)?;
329 }
330 self.profiler
331 .record_hessian_computation(n, started.elapsed(), parallel, sparse);
332
333 Ok(hessian)
334 }
335
336 pub fn hessian_reverse_over_forward(
351 &mut self,
352 function: impl Fn(&Array1<T>) -> T,
353 point: &Array1<T>,
354 _config: &HessianConfig,
355 ) -> Result<Array2<T>> {
356 let n = point.len();
357 let mut hessian = Array2::zeros((n, n));
358 let inner_step = self.fd_step(2);
359
360 for j in 0..n {
362 let partial_fn = |x: &Array1<T>| -> T {
364 let mut x_plus = x.clone();
365 let mut x_minus = x.clone();
366
367 x_plus[j] = x_plus[j] + inner_step;
368 x_minus[j] = x_minus[j] - inner_step;
369
370 (function(&x_plus) - function(&x_minus))
371 / (scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero())
372 * inner_step)
373 };
374
375 let hessian_col = self.gradient_at_point(&partial_fn, point)?;
377
378 for i in 0..n {
379 hessian[[i, j]] = hessian_col[i];
380 }
381 }
382
383 Ok(hessian)
384 }
385
386 pub fn hessian_diagonal(
388 &self,
389 function: &impl Fn(&Array1<T>) -> T,
390 point: &Array1<T>,
391 ) -> Result<Array2<T>> {
392 let n = point.len();
393 let mut hessian = Array2::zeros((n, n));
394 let h = self.fd_step(2);
395
396 for i in 0..n {
398 let mut x_plus = point.clone();
399 let mut x_minus = point.clone();
400
401 x_plus[i] = x_plus[i] + h;
402 x_minus[i] = x_minus[i] - h;
403
404 let f_plus = function(&x_plus);
405 let f_center = function(point);
406 let f_minus = function(&x_minus);
407
408 let second_deriv = (f_plus
410 - scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) * f_center
411 + f_minus)
412 / (h * h);
413
414 hessian[[i, i]] = second_deriv;
415 }
416
417 Ok(hessian)
418 }
419
420 pub fn sparse_hessian(
422 &mut self,
423 function: impl Fn(&Array1<T>) -> T,
424 point: &Array1<T>,
425 config: &HessianConfig,
426 ) -> Result<SparseHessian<T>> {
427 let dense_hessian = self.hessian_forward_over_reverse(function, point, config)?;
428
429 let mut rows = Vec::new();
430 let mut cols = Vec::new();
431 let mut values = Vec::new();
432
433 let threshold = scirs2_core::numeric::NumCast::from(config.sparsity_threshold)
434 .unwrap_or_else(|| T::zero());
435
436 for i in 0..dense_hessian.nrows() {
437 for j in 0..dense_hessian.ncols() {
438 let val = dense_hessian[[i, j]];
439 if val.abs() > threshold {
440 rows.push(i);
441 cols.push(j);
442 values.push(val);
443 }
444 }
445 }
446
447 let nnz = values.len();
448 Ok(SparseHessian {
449 rows,
450 cols,
451 values,
452 shape: dense_hessian.dim(),
453 nnz,
454 })
455 }
456
457 pub fn third_order_derivatives(
459 &mut self,
460 function: impl Fn(&Array1<T>) -> T,
461 point: &Array1<T>,
462 ) -> Result<ThirdOrderTensor<T>> {
463 let n = point.len();
464 let mut tensor = Array3::zeros((n, n, n));
465
466 for i in 0..n {
468 for j in 0..n {
469 for k in 0..n {
470 let third_deriv = self.compute_third_partial(&function, point, i, j, k)?;
471 tensor[[i, j, k]] = third_deriv;
472 }
473 }
474 }
475
476 Ok(ThirdOrderTensor {
477 data: tensor.clone(),
478 shape: tensor.dim(),
479 })
480 }
481
482 pub fn mixed_partial(
484 &mut self,
485 function: impl Fn(&Array1<T>) -> T,
486 point: &Array1<T>,
487 variables: &[usize],
488 orders: &[usize],
489 method: MixedPartialMethod,
490 ) -> Result<MixedPartials<T>> {
491 if variables.len() != orders.len() {
492 return Err(OptimError::InvalidConfig(
493 "Variables and orders length mismatch".to_string(),
494 ));
495 }
496
497 let value = match method {
498 MixedPartialMethod::FiniteDifference => {
499 self.mixed_partial_finite_difference(&function, point, variables, orders)?
500 }
501 MixedPartialMethod::ForwardFiniteDifference => {
502 self.mixed_partial_forward_difference(&function, point, variables, orders)?
503 }
504 MixedPartialMethod::NestedAutodiff => {
505 return Err(OptimError::InvalidConfig(
506 hvp::NESTED_AUTODIFF_UNAVAILABLE.to_string(),
507 ))
508 }
509 };
510
511 Ok(MixedPartials {
512 variables: variables.to_vec(),
513 orders: orders.to_vec(),
514 value,
515 method,
516 })
517 }
518
519 pub fn hessian_vector_product(
521 &mut self,
522 function: impl Fn(&Array1<T>) -> T,
523 point: &Array1<T>,
524 vector: &Array1<T>,
525 ) -> Result<Array1<T>> {
526 let grad_fn = |x: &Array1<T>| -> Array1<T> {
528 self.gradient_at_point(&function, x)
529 .unwrap_or_else(|_| Array1::zeros(x.len()))
530 };
531
532 self.directional_derivative_of_gradient(&grad_fn, point, vector)
533 }
534
535 pub fn vector_hessian_vector_product(
537 &mut self,
538 function: impl Fn(&Array1<T>) -> T,
539 point: &Array1<T>,
540 vector: &Array1<T>,
541 ) -> Result<T> {
542 let hv = self.hessian_vector_product(function, point, vector)?;
543 Ok(vector.dot(&hv))
544 }
545
546 pub fn verify_derivatives(
548 &self,
549 function: impl Fn(&Array1<T>) -> T,
550 point: &Array1<T>,
551 computed_hessian: &Array2<T>,
552 ) -> Result<DerivativeVerification<T>> {
553 let fd_hessian = self.finite_difference_hessian(&function, point)?;
554
555 let max_error = self.compute_max_error(computed_hessian, &fd_hessian);
556 let avg_error = self.compute_avg_error(computed_hessian, &fd_hessian);
557 let relative_error = self.compute_relative_error(computed_hessian, &fd_hessian);
558
559 let is_accurate =
560 max_error < scirs2_core::numeric::NumCast::from(1e-4).unwrap_or_else(|| T::zero());
561
562 Ok(DerivativeVerification {
563 max_absolute_error: max_error,
564 avg_absolute_error: avg_error,
565 max_relative_error: relative_error,
566 is_accurate,
567 finite_diff_hessian: fd_hessian,
568 })
569 }
570
571 pub fn get_derivative_stats(&self) -> HigherOrderStats {
573 HigherOrderStats {
574 _maxorder: self._maxorder,
575 mixed_mode_enabled: self.mixed_mode,
576 memory_usage_estimate: self.estimate_memory_usage(),
577 parallel_computation: self.parallel_computation,
578 thread_pool_size: self.thread_pool_size,
579 adaptive_sparsity: self.adaptive_sparsity,
580 auto_mode_selection: self.auto_mode_selection,
581 performance_profile: self.profiler.get_summary(),
582 }
583 }
584
585 pub fn hessian_vector_product_advanced(
587 &mut self,
588 function: impl Fn(&Array1<T>) -> T + Send + Sync,
589 point: &Array1<T>,
590 vector: &Array1<T>,
591 mode: Option<HvpMode>,
592 ) -> Result<Array1<T>> {
593 let n = point.len();
594 let selected_mode = mode.unwrap_or_else(|| self.select_optimal_hvp_mode(n));
595
596 let started = std::time::Instant::now();
597 let product = match selected_mode {
598 HvpMode::CentralDifference => self.hvp_central_difference(&function, point, vector),
599 HvpMode::ForwardDifference => self.hvp_forward_difference(&function, point, vector),
600 HvpMode::QuadraticSecant => self.hvp_quadratic_secant(&function, point, vector),
601 HvpMode::MaterializedHessian => self.hvp_materialized_hessian(&function, point, vector),
602 HvpMode::NestedAutodiff => Err(OptimError::InvalidConfig(
603 hvp::NESTED_AUTODIFF_UNAVAILABLE.to_string(),
604 )),
605 }?;
606 self.profiler.record_hvp_computation(n, started.elapsed());
609 Ok(product)
610 }
611
612 pub fn vector_hessian_vector_efficient(
614 &mut self,
615 function: impl Fn(&Array1<T>) -> T,
616 point: &Array1<T>,
617 vector: &Array1<T>,
618 ) -> Result<T> {
619 let grad_fn = |x: &Array1<T>| -> Array1<T> {
621 self.gradient_at_point(&function, x)
622 .unwrap_or_else(|_| Array1::zeros(x.len()))
623 };
624
625 let directional_grad = self.directional_derivative_of_gradient(&grad_fn, point, vector)?;
626 Ok(vector.dot(&directional_grad))
627 }
628
629 pub fn jacobian_efficient<F>(
631 &mut self,
632 function: F,
633 point: &Array1<T>,
634 output_dim: usize,
635 ) -> Result<Array2<T>>
636 where
637 F: Fn(&Array1<T>) -> Array1<T> + Send + Sync,
638 {
639 let input_dim = point.len();
640
641 let started = std::time::Instant::now();
642 let jacobian = if input_dim <= output_dim {
644 self.jacobian_forward_mode(&function, point, output_dim)?
646 } else {
647 self.jacobian_reverse_mode(&function, point, output_dim)?
649 };
650 self.profiler
651 .record_jacobian_computation(input_dim, started.elapsed());
652 Ok(jacobian)
653 }
654
655 pub fn kfac_hessian_approximation(
657 &mut self,
658 layers: &[LayerInfo<T>],
659 activations: &[Array1<T>],
660 gradients: &[Array1<T>],
661 ) -> Result<Array2<T>> {
662 if activations.len() != layers.len() || gradients.len() != layers.len() {
666 return Err(OptimError::InvalidConfig(format!(
667 "K-FAC needs one activation and one gradient per layer: {} layers, \
668 {} activations, {} gradients",
669 layers.len(),
670 activations.len(),
671 gradients.len()
672 )));
673 }
674 if layers.is_empty() {
675 return Err(OptimError::InvalidConfig(
676 "K-FAC needs at least one layer".to_string(),
677 ));
678 }
679
680 let mut kfac_blocks = Vec::with_capacity(layers.len());
681
682 for (i, layer) in layers.iter().enumerate() {
683 let activation = &activations[i];
684 let gradient = &gradients[i];
685
686 if activation.len() != layer.input_size {
695 return Err(OptimError::InvalidConfig(format!(
696 "K-FAC layer {i} ({:?}) declares input_size {} but its activation \
697 vector has {} entries",
698 layer.layer_type,
699 layer.input_size,
700 activation.len()
701 )));
702 }
703 if gradient.len() != layer.output_size {
704 return Err(OptimError::InvalidConfig(format!(
705 "K-FAC layer {i} ({:?}) declares output_size {} but its gradient \
706 vector has {} entries",
707 layer.layer_type,
708 layer.output_size,
709 gradient.len()
710 )));
711 }
712
713 let factor_a = self.compute_activation_factor(activation)?;
715 let factor_g = self.compute_gradient_factor(gradient)?;
716
717 let block = self.kronecker_product_approximation(&factor_a, &factor_g)?;
719 kfac_blocks.push(block);
720 }
721
722 self.combine_kfac_blocks(&kfac_blocks)
724 }
725
726 pub fn natural_gradient(
728 &mut self,
729 log_likelihood: impl Fn(&Array1<T>) -> T,
730 point: &Array1<T>,
731 gradient: &Array1<T>,
732 ) -> Result<Array1<T>> {
733 let fim = self.compute_fisher_information_matrix(&log_likelihood, point)?;
735
736 self.solve_linear_system(&fim, gradient)
738 }
739
740 pub fn truncated_newton_direction(
742 &mut self,
743 function: impl Fn(&Array1<T>) -> T + Send + Sync,
744 point: &Array1<T>,
745 gradient: &Array1<T>,
746 max_cg_iterations: usize,
747 cg_tolerance: T,
748 ) -> Result<Array1<T>> {
749 let neg_gradient = gradient.mapv(|x| -x);
751
752 let point_copy = point.clone();
754 let function_copy = function;
755
756 let hvp_fn = move |v: &Array1<T>| -> Result<Array1<T>> {
765 if v.len() != point_copy.len() {
766 return Err(OptimError::InvalidConfig(format!(
767 "CG direction has length {} but the point has {}",
768 v.len(),
769 point_copy.len()
770 )));
771 }
772 let scale = v.iter().fold(T::one(), |acc, value| {
775 let magnitude = value.abs();
776 if magnitude > acc {
777 magnitude
778 } else {
779 acc
780 }
781 });
782 let eps = central_step::<T>(2) / scale;
783 let two: T = scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::one());
784
785 let grad_plus =
786 Self::finite_diff_gradient(&function_copy, &(&point_copy + &(v * eps)))?;
787 let grad_minus =
788 Self::finite_diff_gradient(&function_copy, &(&point_copy - &(v * eps)))?;
789
790 Ok((grad_plus - grad_minus) / (two * eps))
791 };
792
793 self.conjugate_gradient_solve(hvp_fn, &neg_gradient, max_cg_iterations, cg_tolerance)
794 }
795
796 fn finite_diff_gradient<F>(function: &F, point: &Array1<T>) -> Result<Array1<T>>
798 where
799 F: Fn(&Array1<T>) -> T,
800 {
801 let eps = central_step::<T>(1);
802 let mut gradient = Array1::zeros(point.len());
803
804 for i in 0..point.len() {
805 let mut point_plus = point.clone();
806 let mut point_minus = point.clone();
807
808 point_plus[i] = point_plus[i] + eps;
809 point_minus[i] = point_minus[i] - eps;
810
811 let loss_plus = function(&point_plus);
812 let loss_minus = function(&point_minus);
813
814 gradient[i] = (loss_plus - loss_minus)
815 / (scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) * eps);
816 }
817
818 Ok(gradient)
819 }
820
821 fn gradient_at_point(
824 &self,
825 function: &impl Fn(&Array1<T>) -> T,
826 point: &Array1<T>,
827 ) -> Result<Array1<T>> {
828 let n = point.len();
829 let mut gradient = Array1::zeros(n);
830 let h = self.fd_step(1);
831
832 for i in 0..n {
833 let mut x_plus = point.clone();
834 let mut x_minus = point.clone();
835
836 x_plus[i] = x_plus[i] + h;
837 x_minus[i] = x_minus[i] - h;
838
839 gradient[i] = (function(&x_plus) - function(&x_minus))
840 / (scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) * h);
841 }
842
843 Ok(gradient)
844 }
845
846 fn directional_derivative_of_gradient(
847 &self,
848 grad_fn: &impl Fn(&Array1<T>) -> Array1<T>,
849 point: &Array1<T>,
850 direction: &Array1<T>,
851 ) -> Result<Array1<T>> {
852 let eps = self.fd_step(2);
854 let point_plus = point + &(direction * eps);
855 let point_minus = point - &(direction * eps);
856
857 let grad_plus = grad_fn(&point_plus);
858 let grad_minus = grad_fn(&point_minus);
859
860 Ok((grad_plus - grad_minus)
861 / (scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero()) * eps))
862 }
863
864 fn finite_difference_hessian(
865 &self,
866 function: &impl Fn(&Array1<T>) -> T,
867 point: &Array1<T>,
868 ) -> Result<Array2<T>> {
869 let n = point.len();
870 let mut hessian = Array2::zeros((n, n));
871 let eps = self.fd_step(2);
876 let f_center = function(point);
879
880 for i in 0..n {
881 for j in i..n {
882 let second_deriv = if i == j {
884 let mut x_plus = point.clone();
886 let mut x_minus = point.clone();
887
888 x_plus[i] = x_plus[i] + eps;
889 x_minus[i] = x_minus[i] - eps;
890
891 let f_plus = function(&x_plus);
892 let f_minus = function(&x_minus);
893
894 (f_plus
895 - scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero())
896 * f_center
897 + f_minus)
898 / (eps * eps)
899 } else {
900 let mut x_pp = point.clone();
902 x_pp[i] = x_pp[i] + eps;
903 x_pp[j] = x_pp[j] + eps;
904
905 let mut x_pm = point.clone();
906 x_pm[i] = x_pm[i] + eps;
907 x_pm[j] = x_pm[j] - eps;
908
909 let mut x_mp = point.clone();
910 x_mp[i] = x_mp[i] - eps;
911 x_mp[j] = x_mp[j] + eps;
912
913 let mut x_mm = point.clone();
914 x_mm[i] = x_mm[i] - eps;
915 x_mm[j] = x_mm[j] - eps;
916
917 (function(&x_pp) - function(&x_pm) - function(&x_mp) + function(&x_mm))
918 / (scirs2_core::numeric::NumCast::from(4.0).unwrap_or_else(|| T::zero())
919 * eps
920 * eps)
921 };
922
923 hessian[[i, j]] = second_deriv;
924 hessian[[j, i]] = second_deriv; }
926 }
927
928 Ok(hessian)
929 }
930
931 fn compute_third_partial(
944 &self,
945 function: &impl Fn(&Array1<T>) -> T,
946 point: &Array1<T>,
947 i: usize,
948 j: usize,
949 k: usize,
950 ) -> Result<T> {
951 let n = point.len();
952 if i >= n || j >= n || k >= n {
953 return Err(OptimError::InvalidConfig(
954 "third-derivative index out of range".to_string(),
955 ));
956 }
957 let h = self.fd_step(3);
958 let two: T = scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero());
959 let eight: T = scirs2_core::numeric::NumCast::from(8.0).unwrap_or_else(|| T::zero());
960 let h3 = h * h * h;
961
962 let shifted = |shifts: &[(usize, i32)]| -> T {
964 let mut x = point.clone();
965 for &(coord, mult) in shifts {
966 let step: T =
967 scirs2_core::numeric::NumCast::from(mult).unwrap_or_else(|| T::zero());
968 x[coord] = x[coord] + step * h;
969 }
970 function(&x)
971 };
972
973 let all_same = i == j && j == k;
974 let all_distinct = i != j && j != k && i != k;
975
976 let value = if all_same {
977 let a = shifted(&[(i, 2)]);
978 let b = shifted(&[(i, 1)]);
979 let c = shifted(&[(i, -1)]);
980 let d = shifted(&[(i, -2)]);
981 (a - two * b + two * c - d) / (two * h3)
982 } else if all_distinct {
983 let mut acc = T::zero();
984 for &si in &[1i32, -1] {
985 for &sj in &[1i32, -1] {
986 for &sk in &[1i32, -1] {
987 let sign: T = scirs2_core::numeric::NumCast::from(si * sj * sk)
988 .unwrap_or_else(|| T::zero());
989 acc = acc + sign * shifted(&[(i, si), (j, sj), (k, sk)]);
990 }
991 }
992 }
993 acc / (eight * h3)
994 } else {
995 let (r, s) = if i == j {
997 (i, k)
998 } else if i == k {
999 (i, j)
1000 } else {
1001 (j, i)
1002 };
1003 let d2_plus =
1004 shifted(&[(r, 1), (s, 1)]) - two * shifted(&[(s, 1)]) + shifted(&[(r, -1), (s, 1)]);
1005 let d2_minus = shifted(&[(r, 1), (s, -1)]) - two * shifted(&[(s, -1)])
1006 + shifted(&[(r, -1), (s, -1)]);
1007 (d2_plus - d2_minus) / (two * h3)
1008 };
1009
1010 Ok(value)
1011 }
1012
1013 fn mixed_partial_finite_difference(
1022 &self,
1023 function: &impl Fn(&Array1<T>) -> T,
1024 point: &Array1<T>,
1025 variables: &[usize],
1026 orders: &[usize],
1027 ) -> Result<T> {
1028 let total_order: usize = orders.iter().sum();
1029 if total_order > 3 {
1030 return Err(OptimError::InvalidConfig(
1031 "Mixed partial order too high (finite differences support order <= 3)".to_string(),
1032 ));
1033 }
1034 let n = point.len();
1035 for &v in variables {
1036 if v >= n {
1037 return Err(OptimError::InvalidConfig(
1038 "mixed-partial variable index out of range".to_string(),
1039 ));
1040 }
1041 }
1042
1043 let mut flat: Vec<usize> = Vec::with_capacity(total_order);
1045 for (&v, &o) in variables.iter().zip(orders.iter()) {
1046 for _ in 0..o {
1047 flat.push(v);
1048 }
1049 }
1050
1051 let two: T = scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::zero());
1052
1053 match flat.as_slice() {
1054 [] => Ok(function(point)),
1055 &[i] => {
1056 let h = self.fd_step(1);
1057 let mut xp = point.clone();
1058 let mut xm = point.clone();
1059 xp[i] = xp[i] + h;
1060 xm[i] = xm[i] - h;
1061 Ok((function(&xp) - function(&xm)) / (two * h))
1062 }
1063 &[i, j] => {
1064 let h = self.fd_step(2);
1065 if i == j {
1066 let mut xp = point.clone();
1068 let mut xm = point.clone();
1069 xp[i] = xp[i] + h;
1070 xm[i] = xm[i] - h;
1071 Ok((function(&xp) - two * function(point) + function(&xm)) / (h * h))
1072 } else {
1073 let four: T =
1075 scirs2_core::numeric::NumCast::from(4.0).unwrap_or_else(|| T::zero());
1076 let mut x_pp = point.clone();
1077 x_pp[i] = x_pp[i] + h;
1078 x_pp[j] = x_pp[j] + h;
1079 let mut x_pm = point.clone();
1080 x_pm[i] = x_pm[i] + h;
1081 x_pm[j] = x_pm[j] - h;
1082 let mut x_mp = point.clone();
1083 x_mp[i] = x_mp[i] - h;
1084 x_mp[j] = x_mp[j] + h;
1085 let mut x_mm = point.clone();
1086 x_mm[i] = x_mm[i] - h;
1087 x_mm[j] = x_mm[j] - h;
1088 Ok(
1089 (function(&x_pp) - function(&x_pm) - function(&x_mp) + function(&x_mm))
1090 / (four * h * h),
1091 )
1092 }
1093 }
1094 &[i, j, k] => self.compute_third_partial(function, point, i, j, k),
1095 _ => Err(OptimError::InvalidConfig(
1096 "unsupported mixed-partial structure".to_string(),
1097 )),
1098 }
1099 }
1100
1101 fn verify_hessian_accuracy(&self, computed: &Array2<T>, reference: &Array2<T>) -> Result<()> {
1102 let max_error = self.compute_max_error(computed, reference);
1103 let threshold = scirs2_core::numeric::NumCast::from(1e-3).unwrap_or_else(|| T::zero());
1104
1105 if max_error > threshold {
1106 return Err(OptimError::InvalidConfig(format!(
1107 "Hessian verification failed: max error {}",
1108 max_error.to_f64().unwrap_or(0.0)
1109 )));
1110 }
1111
1112 Ok(())
1113 }
1114
1115 fn compute_max_error(&self, a: &Array2<T>, b: &Array2<T>) -> T {
1116 a.iter()
1117 .zip(b.iter())
1118 .map(|(&x, &y)| (x - y).abs())
1119 .fold(T::zero(), |acc, x| if x > acc { x } else { acc })
1120 }
1121
1122 fn compute_avg_error(&self, a: &Array2<T>, b: &Array2<T>) -> T {
1123 let sum = a
1124 .iter()
1125 .zip(b.iter())
1126 .map(|(&x, &y)| (x - y).abs())
1127 .sum::<T>();
1128
1129 let count = scirs2_core::numeric::NumCast::from(a.len()).unwrap_or_else(T::one);
1130 sum / count
1131 }
1132
1133 fn compute_relative_error(&self, a: &Array2<T>, b: &Array2<T>) -> T {
1134 a.iter()
1135 .zip(b.iter())
1136 .map(|(&x, &y)| {
1137 if y.abs() > scirs2_core::numeric::NumCast::from(1e-12).unwrap_or_else(|| T::zero())
1138 {
1139 ((x - y) / y).abs()
1140 } else {
1141 (x - y).abs()
1142 }
1143 })
1144 .fold(T::zero(), |acc, x| if x > acc { x } else { acc })
1145 }
1146
1147 fn estimate_memory_usage(&self) -> usize {
1150 let engine_size = std::mem::size_of::<Self>();
1151 let profiler_size =
1152 self.profiler.recorded_timings() * std::mem::size_of::<ComputationTiming>();
1153
1154 engine_size + profiler_size
1155 }
1156
1157 fn should_use_parallel(&self, problemsize: usize) -> bool {
1159 self.parallel_computation && problemsize >= 50
1160 }
1161
1162 fn should_use_sparse(&self, problemsize: usize, config: &HessianConfig) -> bool {
1164 self.adaptive_sparsity && config.sparse && problemsize >= 100
1165 }
1166
1167 fn select_optimal_hvp_mode(&self, problemsize: usize) -> HvpMode {
1181 if !self.auto_mode_selection {
1182 return HvpMode::CentralDifference;
1183 }
1184
1185 match problemsize {
1186 0..=8 => HvpMode::MaterializedHessian,
1187 _ => HvpMode::CentralDifference,
1188 }
1189 }
1190
1191 fn apply_adaptive_sparsity(&self, mut matrix: Array2<T>, threshold: f64) -> Result<Array2<T>> {
1193 let sparsity_threshold =
1194 scirs2_core::numeric::NumCast::from(threshold).unwrap_or_else(|| T::zero());
1195
1196 for elem in matrix.iter_mut() {
1197 if elem.abs() < sparsity_threshold {
1198 *elem = T::zero();
1199 }
1200 }
1201
1202 Ok(matrix)
1203 }
1204
1205 fn jacobian_forward_mode<F>(
1217 &mut self,
1218 function: &F,
1219 point: &Array1<T>,
1220 output_dim: usize,
1221 ) -> Result<Array2<T>>
1222 where
1223 F: Fn(&Array1<T>) -> Array1<T>,
1224 {
1225 let input_dim = point.len();
1226 let mut jacobian = Array2::zeros((output_dim, input_dim));
1227 let eps = self.fd_step(1);
1228 let two: T = scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::one());
1229
1230 for j in 0..input_dim {
1231 let mut point_plus = point.clone();
1232 let mut point_minus = point.clone();
1233 point_plus[j] = point_plus[j] + eps;
1234 point_minus[j] = point_minus[j] - eps;
1235
1236 let f_plus = function(&point_plus);
1237 let f_minus = function(&point_minus);
1238 if f_plus.len() < output_dim || f_minus.len() < output_dim {
1239 return Err(OptimError::InvalidConfig(format!(
1240 "function returned {} outputs, expected at least {output_dim}",
1241 f_plus.len().min(f_minus.len())
1242 )));
1243 }
1244
1245 let column = (f_plus - f_minus) / (two * eps);
1246
1247 for i in 0..output_dim {
1248 jacobian[[i, j]] = column[i];
1249 }
1250 }
1251
1252 Ok(jacobian)
1253 }
1254
1255 fn jacobian_reverse_mode<F>(
1257 &mut self,
1258 function: &F,
1259 point: &Array1<T>,
1260 output_dim: usize,
1261 ) -> Result<Array2<T>>
1262 where
1263 F: Fn(&Array1<T>) -> Array1<T>,
1264 {
1265 let input_dim = point.len();
1266 let mut jacobian = Array2::zeros((output_dim, input_dim));
1267
1268 for i in 0..output_dim {
1270 let scalar_fn = |x: &Array1<T>| function(x)[i];
1272
1273 let grad = self.gradient_at_point(&scalar_fn, point)?;
1275
1276 for j in 0..input_dim {
1277 jacobian[[i, j]] = grad[j];
1278 }
1279 }
1280
1281 Ok(jacobian)
1282 }
1283
1284 fn compute_activation_factor(&self, activation: &Array1<T>) -> Result<Array2<T>> {
1286 let n = activation.len();
1287 let mut factor = Array2::zeros((n, n));
1288
1289 for i in 0..n {
1291 for j in 0..n {
1292 factor[[i, j]] = activation[i] * activation[j];
1293 }
1294 }
1295
1296 Ok(factor)
1297 }
1298
1299 fn compute_gradient_factor(&self, gradient: &Array1<T>) -> Result<Array2<T>> {
1300 let n = gradient.len();
1301 let mut factor = Array2::zeros((n, n));
1302
1303 for i in 0..n {
1305 for j in 0..n {
1306 factor[[i, j]] = gradient[i] * gradient[j];
1307 }
1308 }
1309
1310 Ok(factor)
1311 }
1312
1313 fn kronecker_product_approximation(&self, a: &Array2<T>, g: &Array2<T>) -> Result<Array2<T>> {
1314 let n_a = a.nrows();
1317 let n_g = g.nrows();
1318 let n_total = n_a * n_g;
1319
1320 let mut result = Array2::zeros((n_total, n_total));
1321
1322 for i in 0..n_a {
1323 for j in 0..n_a {
1324 for k in 0..n_g {
1325 for l in 0..n_g {
1326 let row = i * n_g + k;
1327 let col = j * n_g + l;
1328 result[[row, col]] = a[[i, j]] * g[[k, l]];
1329 }
1330 }
1331 }
1332 }
1333
1334 Ok(result)
1335 }
1336
1337 fn combine_kfac_blocks(&self, blocks: &[Array2<T>]) -> Result<Array2<T>> {
1338 if blocks.is_empty() {
1339 return Err(OptimError::InvalidConfig("Empty K-FAC blocks".to_string()));
1340 }
1341
1342 let total_size: usize = blocks.iter().map(|b| b.nrows()).sum();
1344 let mut combined = Array2::zeros((total_size, total_size));
1345
1346 let mut row_offset = 0;
1347 let mut col_offset = 0;
1348
1349 for block in blocks {
1350 let block_size = block.nrows();
1351
1352 for i in 0..block_size {
1353 for j in 0..block_size {
1354 combined[[row_offset + i, col_offset + j]] = block[[i, j]];
1355 }
1356 }
1357
1358 row_offset += block_size;
1359 col_offset += block_size;
1360 }
1361
1362 Ok(combined)
1363 }
1364
1365 fn compute_fisher_information_matrix(
1366 &mut self,
1367 log_likelihood: &impl Fn(&Array1<T>) -> T,
1368 point: &Array1<T>,
1369 ) -> Result<Array2<T>> {
1370 self.hessian_forward_over_reverse(log_likelihood, point, &HessianConfig::default())
1373 }
1374
1375 fn solve_linear_system(&self, matrix: &Array2<T>, rhs: &Array1<T>) -> Result<Array1<T>> {
1383 let n = matrix.nrows();
1384 if n != rhs.len() || n != matrix.ncols() {
1385 return Err(OptimError::InvalidConfig(
1386 "Matrix dimension mismatch".to_string(),
1387 ));
1388 }
1389 if n == 0 {
1390 return Ok(Array1::zeros(0));
1391 }
1392
1393 let mut diag_sum = T::zero();
1394 for i in 0..n {
1395 diag_sum = diag_sum + matrix[[i, i]].abs();
1396 }
1397 let n_t: T = scirs2_core::numeric::NumCast::from(n).unwrap_or_else(|| T::one());
1398 let ridge_scale: T = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero());
1399 let ridge_floor: T =
1400 scirs2_core::numeric::NumCast::from(1e-12).unwrap_or_else(|| T::zero());
1401 let ridge = (diag_sum / n_t) * ridge_scale + ridge_floor;
1402
1403 let hvp = |v: &Array1<T>| -> Result<Array1<T>> { Ok(matrix.dot(v) + &(v * ridge)) };
1404
1405 let tolerance: T = scirs2_core::numeric::NumCast::from(1e-10).unwrap_or_else(|| T::zero());
1406 self.conjugate_gradient_solve(hvp, rhs, 2 * n + 50, tolerance)
1407 }
1408
1409 fn conjugate_gradient_solve<F>(
1410 &self,
1411 mut hvp_fn: F,
1412 rhs: &Array1<T>,
1413 max_iterations: usize,
1414 tolerance: T,
1415 ) -> Result<Array1<T>>
1416 where
1417 F: FnMut(&Array1<T>) -> Result<Array1<T>>,
1418 {
1419 let n = rhs.len();
1420 let mut x = Array1::zeros(n);
1421 let mut r = rhs.clone();
1422 let mut p = r.clone();
1423 let mut rsold = r.dot(&r);
1424
1425 for _iter in 0..max_iterations {
1426 let ap = hvp_fn(&p)?;
1427 let denom = p.dot(&ap);
1428 if denom.abs() <= T::epsilon() {
1430 break;
1431 }
1432 let alpha = rsold / denom;
1433
1434 x = x + &p * alpha;
1435 r = r - &ap * alpha;
1436
1437 let rsnew = r.dot(&r);
1438
1439 if rsnew.sqrt() < tolerance {
1440 break;
1441 }
1442
1443 let beta = rsnew / rsold;
1444 p = &r + &p * beta;
1445 rsold = rsnew;
1446 }
1447
1448 Ok(x)
1449 }
1450}
1451
1452#[derive(Debug, Clone)]
1454pub struct DerivativeVerification<T: Float + Debug + Send + Sync + 'static> {
1455 pub max_absolute_error: T,
1456 pub avg_absolute_error: T,
1457 pub max_relative_error: T,
1458 pub is_accurate: bool,
1459 pub finite_diff_hessian: Array2<T>,
1460}
1461
1462#[derive(Debug, Clone)]
1464pub struct HigherOrderStats {
1465 pub _maxorder: usize,
1466 pub mixed_mode_enabled: bool,
1467 pub memory_usage_estimate: usize,
1468 pub parallel_computation: bool,
1469 pub thread_pool_size: usize,
1470 pub adaptive_sparsity: bool,
1471 pub auto_mode_selection: bool,
1472 pub performance_profile: PerformanceProfile,
1473}
1474
1475#[derive(Debug, Clone)]
1477pub struct HigherOrderConfig<T: Float + Debug + Send + Sync + 'static> {
1478 pub _maxorder: usize,
1479 pub mixed_mode: bool,
1480 pub finite_diff_eps: T,
1481 pub parallel_computation: bool,
1482 pub thread_pool_size: usize,
1483 pub adaptive_sparsity: bool,
1484 pub auto_mode_selection: bool,
1485 pub cache_size_limit: usize,
1486}
1487
1488impl<T: Float + Debug + Default + Send + Sync> Default for HigherOrderConfig<T> {
1489 fn default() -> Self {
1490 Self {
1491 _maxorder: 3,
1492 mixed_mode: true,
1493 finite_diff_eps: scirs2_core::numeric::NumCast::from(1e-5).unwrap_or_else(|| T::zero()),
1494 parallel_computation: true,
1495 thread_pool_size: 4, adaptive_sparsity: true,
1497 auto_mode_selection: true,
1498 cache_size_limit: 10000,
1499 }
1500 }
1501}
1502
1503#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1512pub enum HvpMode {
1513 CentralDifference,
1516
1517 ForwardDifference,
1520
1521 QuadraticSecant,
1524
1525 MaterializedHessian,
1529
1530 NestedAutodiff,
1535}
1536
1537#[derive(Debug, Clone)]
1539pub struct ComputationProfiler<T: Float + Debug + Send + Sync + 'static> {
1540 hessian_timings: Vec<ComputationTiming>,
1541 hvp_timings: Vec<ComputationTiming>,
1542 jacobian_timings: Vec<ComputationTiming>,
1543 total_computations: usize,
1544 _phantom: std::marker::PhantomData<T>,
1545}
1546
1547const HESSIAN_METHOD: &str = "hessian";
1549const HVP_METHOD: &str = "hvp";
1551const JACOBIAN_METHOD: &str = "jacobian";
1553
1554#[derive(Debug, Clone)]
1555struct ComputationTiming {
1556 problemsize: usize,
1557 duration_us: u64,
1558 parallel: bool,
1559 sparse: bool,
1560 method: &'static str,
1561}
1562
1563#[derive(Debug, Clone)]
1570pub struct PerformanceProfile {
1571 pub avg_hessian_time_us: f64,
1573 pub avg_hvp_time_us: f64,
1575 pub avg_jacobian_time_us: f64,
1577 pub parallel_efficiency: f64,
1580 pub sparsity_benefit: f64,
1583 pub recorded_computations: usize,
1585 pub largest_hessian_problem: usize,
1587}
1588
1589impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> ComputationProfiler<T> {
1590 fn new() -> Self {
1591 Self {
1592 hessian_timings: Vec::new(),
1593 hvp_timings: Vec::new(),
1594 jacobian_timings: Vec::new(),
1595 total_computations: 0,
1596 _phantom: std::marker::PhantomData,
1597 }
1598 }
1599
1600 fn record_hessian_computation(
1601 &mut self,
1602 size: usize,
1603 duration: std::time::Duration,
1604 parallel: bool,
1605 sparse: bool,
1606 ) {
1607 self.hessian_timings.push(ComputationTiming {
1608 problemsize: size,
1609 duration_us: duration.as_micros() as u64,
1610 parallel,
1611 sparse,
1612 method: HESSIAN_METHOD,
1613 });
1614 self.total_computations += 1;
1615 }
1616
1617 fn record_hvp_computation(&mut self, size: usize, duration: std::time::Duration) {
1618 self.hvp_timings.push(ComputationTiming {
1619 problemsize: size,
1620 duration_us: duration.as_micros() as u64,
1621 parallel: false,
1622 sparse: false,
1623 method: HVP_METHOD,
1624 });
1625 self.total_computations += 1;
1626 }
1627
1628 fn record_jacobian_computation(&mut self, size: usize, duration: std::time::Duration) {
1629 self.jacobian_timings.push(ComputationTiming {
1630 problemsize: size,
1631 duration_us: duration.as_micros() as u64,
1632 parallel: false,
1633 sparse: false,
1634 method: JACOBIAN_METHOD,
1635 });
1636 self.total_computations += 1;
1637 }
1638
1639 fn total_computations(&self) -> usize {
1641 self.total_computations
1642 }
1643
1644 fn recorded_timings(&self) -> usize {
1646 self.hessian_timings.len() + self.hvp_timings.len() + self.jacobian_timings.len()
1647 }
1648
1649 fn mean_duration_us(timings: &[ComputationTiming]) -> f64 {
1651 if timings.is_empty() {
1652 return 0.0;
1653 }
1654 timings.iter().map(|t| t.duration_us as f64).sum::<f64>() / timings.len() as f64
1655 }
1656
1657 fn largest_problem(&self, method: &'static str) -> usize {
1659 self.hessian_timings
1660 .iter()
1661 .chain(self.hvp_timings.iter())
1662 .chain(self.jacobian_timings.iter())
1663 .filter(|t| t.method == method)
1664 .map(|t| t.problemsize)
1665 .max()
1666 .unwrap_or(0)
1667 }
1668
1669 fn get_summary(&self) -> PerformanceProfile {
1670 PerformanceProfile {
1671 avg_hessian_time_us: Self::mean_duration_us(&self.hessian_timings),
1672 avg_hvp_time_us: Self::mean_duration_us(&self.hvp_timings),
1673 avg_jacobian_time_us: Self::mean_duration_us(&self.jacobian_timings),
1674 parallel_efficiency: self.compute_parallel_efficiency(),
1675 sparsity_benefit: self.compute_sparsity_benefit(),
1676 recorded_computations: self.total_computations(),
1677 largest_hessian_problem: self.largest_problem(HESSIAN_METHOD),
1678 }
1679 }
1680
1681 fn compute_parallel_efficiency(&self) -> f64 {
1682 if self.hessian_timings.is_empty() {
1683 return 1.0;
1684 }
1685
1686 let parallel_times: Vec<_> = self.hessian_timings.iter().filter(|t| t.parallel).collect();
1687 let sequential_times: Vec<_> = self
1688 .hessian_timings
1689 .iter()
1690 .filter(|t| !t.parallel)
1691 .collect();
1692
1693 if parallel_times.is_empty() || sequential_times.is_empty() {
1694 return 1.0;
1695 }
1696
1697 let avg_parallel = parallel_times
1698 .iter()
1699 .map(|t| t.duration_us as f64)
1700 .sum::<f64>()
1701 / parallel_times.len() as f64;
1702 let avg_sequential = sequential_times
1703 .iter()
1704 .map(|t| t.duration_us as f64)
1705 .sum::<f64>()
1706 / sequential_times.len() as f64;
1707
1708 avg_sequential / avg_parallel
1709 }
1710
1711 fn compute_sparsity_benefit(&self) -> f64 {
1718 let sparse = Self::mean_duration_us(
1719 &self
1720 .hessian_timings
1721 .iter()
1722 .filter(|t| t.sparse)
1723 .cloned()
1724 .collect::<Vec<_>>(),
1725 );
1726 let dense = Self::mean_duration_us(
1727 &self
1728 .hessian_timings
1729 .iter()
1730 .filter(|t| !t.sparse)
1731 .cloned()
1732 .collect::<Vec<_>>(),
1733 );
1734 if sparse <= 0.0 || dense <= 0.0 {
1735 return 1.0;
1736 }
1737 dense / sparse
1738 }
1739}
1740
1741#[cfg(test)]
1742mod tests {
1743 use super::*;
1744
1745 #[test]
1746 fn test_higher_order_engine_creation() {
1747 let engine = HigherOrderEngine::<f64>::new(3);
1748 assert_eq!(engine._maxorder, 3);
1749 assert!(engine.mixed_mode);
1750 }
1751
1752 #[test]
1753 fn test_hessian_diagonal() {
1754 let engine = HigherOrderEngine::<f64>::new(2);
1755
1756 let function = |x: &Array1<f64>| x[0] * x[0] + 2.0 * x[1] * x[1];
1758 let point = Array1::from_vec(vec![1.0, 1.0]);
1759
1760 let hessian = engine
1761 .hessian_diagonal(&function, &point)
1762 .expect("hessian_diagonal should succeed");
1763
1764 assert!((hessian[[0, 0]] - 2.0).abs() < 1e-5);
1766 assert!((hessian[[1, 1]] - 4.0).abs() < 1e-5);
1767 assert!((hessian[[0, 1]]).abs() < 1e-10); }
1769
1770 #[test]
1771 fn test_finite_difference_hessian() {
1772 let engine = HigherOrderEngine::<f64>::new(2);
1773
1774 let function = |x: &Array1<f64>| x[0] * x[0] + x[0] * x[1] + x[1] * x[1];
1776 let point = Array1::from_vec(vec![1.0, 1.0]);
1777
1778 let hessian = engine
1779 .finite_difference_hessian(&function, &point)
1780 .expect("finite_difference_hessian should succeed");
1781
1782 assert!((hessian[[0, 0]] - 2.0).abs() < 1e-5);
1784 assert!((hessian[[0, 1]] - 1.0).abs() < 1e-5);
1785 assert!((hessian[[1, 0]] - 1.0).abs() < 1e-5);
1786 assert!((hessian[[1, 1]] - 2.0).abs() < 1e-5);
1787 }
1788
1789 #[test]
1790 fn test_mixed_partial() {
1791 let mut engine = HigherOrderEngine::<f64>::new(2);
1792
1793 let function = |x: &Array1<f64>| x[0] * x[0] * x[1] + x[0] * x[1] * x[1];
1795 let point = Array1::from_vec(vec![1.0, 1.0]);
1796
1797 let mixed_partial = engine
1798 .mixed_partial(
1799 function,
1800 &point,
1801 &[0, 1],
1802 &[1, 1],
1803 MixedPartialMethod::FiniteDifference,
1804 )
1805 .expect("mixed_partial should succeed");
1806
1807 assert!((mixed_partial.value - 4.0).abs() < 1e-6);
1809 }
1810
1811 #[test]
1812 fn test_sparse_hessian() {
1813 let mut engine = HigherOrderEngine::<f64>::new(2);
1814
1815 let function = |x: &Array1<f64>| x[0] * x[0] + x[1] * x[1];
1817 let point = Array1::from_vec(vec![1.0, 1.0]);
1818
1819 let config = HessianConfig {
1820 sparse: true,
1821 sparsity_threshold: 1e-4,
1822 ..Default::default()
1823 };
1824
1825 let sparse_hessian = engine
1826 .sparse_hessian(function, &point, &config)
1827 .expect("sparse_hessian should succeed");
1828
1829 assert_eq!(sparse_hessian.nnz, 2);
1831 assert_eq!(sparse_hessian.shape, (2, 2));
1832 }
1833
1834 #[test]
1835 fn test_hessian_config_default() {
1836 let config = HessianConfig::default();
1837 assert!(config.exact);
1838 assert!(!config.sparse);
1839 assert!(!config.diagonal_only);
1840 }
1841
1842 #[test]
1846 fn test_higher_order_hessian_of_square() {
1847 let mut engine = HigherOrderEngine::<f64>::new(2);
1848
1849 let function = |x: &Array1<f64>| x[0] * x[0];
1850 let point = Array1::from_vec(vec![3.0]);
1851
1852 let hessian = engine
1853 .hessian_forward_over_reverse(function, &point, &HessianConfig::default())
1854 .expect("hessian");
1855
1856 approx::assert_abs_diff_eq!(hessian[[0, 0]], 2.0, epsilon = 1e-3);
1857 }
1858
1859 #[test]
1864 fn test_higher_order_hvp_quadratic() {
1865 let mut engine = HigherOrderEngine::<f64>::new(2);
1866
1867 let function = |x: &Array1<f64>| 0.5 * (x[0] * x[0] + x[1] * x[1]);
1868 let point = Array1::from_vec(vec![1.0, -2.0]);
1869 let vector = Array1::from_vec(vec![3.0, 4.0]);
1870
1871 let hv = engine
1872 .hessian_vector_product(function, &point, &vector)
1873 .expect("hvp");
1874
1875 approx::assert_abs_diff_eq!(hv[0], 3.0, epsilon = 1e-3);
1876 approx::assert_abs_diff_eq!(hv[1], 4.0, epsilon = 1e-3);
1877 }
1878
1879 #[test]
1888 fn test_third_order_derivatives_are_accurate() {
1889 let mut engine = HigherOrderEngine::<f64>::new(3);
1890 let function =
1891 |x: &Array1<f64>| x[0] * x[0] * x[0] + x[0] * x[1] * x[2] + x[0] * x[0] * x[1];
1892 let point = Array1::from_vec(vec![0.7, -0.4, 1.3]);
1893
1894 let tensor = engine
1895 .third_order_derivatives(function, &point)
1896 .expect("third order");
1897
1898 approx::assert_abs_diff_eq!(tensor.data[[0, 0, 0]], 6.0, epsilon = 1e-3);
1899 approx::assert_abs_diff_eq!(tensor.data[[0, 1, 2]], 1.0, epsilon = 1e-3);
1900 approx::assert_abs_diff_eq!(tensor.data[[0, 0, 1]], 2.0, epsilon = 1e-3);
1901 approx::assert_abs_diff_eq!(tensor.data[[2, 1, 0]], 1.0, epsilon = 1e-3);
1903 }
1904
1905 #[test]
1908 fn test_mixed_partial_supports_orders_one_and_three() {
1909 let mut engine = HigherOrderEngine::<f64>::new(3);
1910 let point = Array1::from_vec(vec![0.5, -0.3, 0.9]);
1911
1912 let g = |x: &Array1<f64>| 3.0 * x[0] + x[1] * x[1];
1914 let d1 = engine
1915 .mixed_partial(g, &point, &[0], &[1], MixedPartialMethod::FiniteDifference)
1916 .expect("order 1");
1917 approx::assert_abs_diff_eq!(d1.value, 3.0, epsilon = 1e-6);
1918
1919 let cube = |x: &Array1<f64>| x[0] * x[0] * x[0];
1921 let d3 = engine
1922 .mixed_partial(
1923 cube,
1924 &point,
1925 &[0],
1926 &[3],
1927 MixedPartialMethod::FiniteDifference,
1928 )
1929 .expect("order 3 pure");
1930 approx::assert_abs_diff_eq!(d3.value, 6.0, epsilon = 1e-3);
1931
1932 let prod = |x: &Array1<f64>| x[0] * x[1] * x[2];
1934 let d3m = engine
1935 .mixed_partial(
1936 prod,
1937 &point,
1938 &[0, 1, 2],
1939 &[1, 1, 1],
1940 MixedPartialMethod::FiniteDifference,
1941 )
1942 .expect("order 3 mixed");
1943 approx::assert_abs_diff_eq!(d3m.value, 1.0, epsilon = 1e-3);
1944 }
1945
1946 #[test]
1949 fn test_solve_linear_system_dense_spd() {
1950 let engine = HigherOrderEngine::<f64>::new(2);
1951 let a = Array2::from_shape_vec((2, 2), vec![4.0, 1.0, 1.0, 3.0]).expect("matrix");
1953 let b = Array1::from_vec(vec![1.0, 2.0]);
1954
1955 let x = engine.solve_linear_system(&a, &b).expect("solve");
1956
1957 approx::assert_abs_diff_eq!(x[0], 1.0 / 11.0, epsilon = 1e-6);
1959 approx::assert_abs_diff_eq!(x[1], 7.0 / 11.0, epsilon = 1e-6);
1960
1961 let residual = a.dot(&x) - &b;
1963 assert!(residual.dot(&residual).sqrt() < 1e-6);
1964 }
1965}