1use ndarray::{Array1, ArrayView1};
7use num_traits::Zero;
8use rand::seq::SliceRandom;
9use rand::{rngs::StdRng, SeedableRng};
10use rayon::prelude::*;
11use scirs2_sparse::{csr_array::CsrArray, sparray::SparseArray};
12
13use crate::error::OptimizeError;
14use crate::parallel::ParallelOptions;
15
16#[derive(Debug, Clone)]
18pub struct SparseFiniteDiffOptions {
19 pub method: String,
21 pub rel_step: Option<f64>,
23 pub abs_step: Option<f64>,
25 pub bounds: Option<Vec<(f64, f64)>>,
27 pub parallel: Option<ParallelOptions>,
29 pub seed: Option<u64>,
31 pub max_group_size: usize,
33}
34
35impl Default for SparseFiniteDiffOptions {
36 fn default() -> Self {
37 Self {
38 method: "2-point".to_string(),
39 rel_step: None,
40 abs_step: None,
41 bounds: None,
42 parallel: None,
43 seed: None,
44 max_group_size: 100,
45 }
46 }
47}
48
49pub fn sparse_jacobian<F>(
64 func: F,
65 x: &ArrayView1<f64>,
66 f0: Option<&Array1<f64>>,
67 sparsity_pattern: Option<&CsrArray<f64>>,
68 options: Option<SparseFiniteDiffOptions>,
69) -> Result<CsrArray<f64>, OptimizeError>
70where
71 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
72{
73 let options = options.unwrap_or_default();
74
75 let f0_owned: Array1<f64>;
77 let f0_ref = match f0 {
78 Some(f) => f,
79 None => {
80 f0_owned = func(x);
81 &f0_owned
82 }
83 };
84
85 let n = x.len(); let m = f0_ref.len(); let sparsity_owned: CsrArray<f64>;
90 let sparsity = match sparsity_pattern {
91 Some(p) => p,
92 None => {
93 let mut data = Vec::with_capacity(m * n);
95 let mut rows = Vec::with_capacity(m * n);
96 let mut cols = Vec::with_capacity(m * n);
97
98 for i in 0..m {
99 for j in 0..n {
100 data.push(1.0);
101 rows.push(i);
102 cols.push(j);
103 }
104 }
105
106 sparsity_owned =
107 CsrArray::from_triplets(&rows, &cols, &data, (m, n), false).map_err(|e| {
108 OptimizeError::ComputationError(format!(
109 "Error creating sparsity pattern: {}",
110 e
111 ))
112 })?;
113 &sparsity_owned
114 }
115 };
116
117 let groups = determine_column_groups(sparsity, options.max_group_size, options.seed);
119
120 let h = compute_step_sizes(x, f0_ref, &options);
122
123 match options.method.as_str() {
125 "2-point" => compute_jacobian_2point(func, x, f0_ref, &h, &groups, sparsity, &options),
126 "3-point" => compute_jacobian_3point(func, x, f0_ref, &h, &groups, sparsity, &options),
127 "cs" => compute_jacobian_complex_step(func, x, f0_ref, &h, &groups, sparsity, &options),
128 _ => Err(OptimizeError::ComputationError(format!(
129 "Unknown method: {}",
130 options.method
131 ))),
132 }
133}
134
135pub fn sparse_hessian<F>(
150 func: F,
151 x: &ArrayView1<f64>,
152 grad: Option<&Array1<f64>>,
153 sparsity_pattern: Option<&CsrArray<f64>>,
154 options: Option<SparseFiniteDiffOptions>,
155) -> Result<CsrArray<f64>, OptimizeError>
156where
157 F: Fn(&ArrayView1<f64>) -> f64 + Sync + Clone,
158{
159 let options = options.unwrap_or_default();
160 let n = x.len();
161
162 fn grad_fn<F: Fn(&ArrayView1<f64>) -> f64 + Clone>(
164 func: &F,
165 x: &ArrayView1<f64>,
166 ) -> Array1<f64> {
167 let mut grad = Array1::zeros(x.len());
169 let eps = 1e-8;
170
171 let f0 = func(x);
172
173 for i in 0..x.len() {
174 let mut x_plus = x.to_owned();
175 x_plus[i] += eps;
176 let f_plus = func(&x_plus.view());
177
178 grad[i] = (f_plus - f0) / eps;
179 }
180
181 grad
182 }
183
184 let grad_owned: Array1<f64>;
186 let grad_ref = match grad {
187 Some(g) => g,
188 None => {
189 grad_owned = grad_fn(&func, x);
190 &grad_owned
191 }
192 };
193
194 let sparsity_owned: CsrArray<f64>;
196 let sparsity = match sparsity_pattern {
197 Some(p) => p,
198 None => {
199 let mut data = Vec::with_capacity(n * (n + 1) / 2);
201 let mut rows = Vec::with_capacity(n * (n + 1) / 2);
202 let mut cols = Vec::with_capacity(n * (n + 1) / 2);
203
204 for i in 0..n {
205 for j in i..n {
206 data.push(1.0);
208 rows.push(i);
209 cols.push(j);
210 }
211 }
212
213 sparsity_owned =
214 CsrArray::from_triplets(&rows, &cols, &data, (n, n), false).map_err(|e| {
215 OptimizeError::ComputationError(format!(
216 "Error creating sparsity pattern: {}",
217 e
218 ))
219 })?;
220 &sparsity_owned
221 }
222 };
223
224 let groups = determine_column_groups(sparsity, options.max_group_size, options.seed);
226
227 let mut single_output = Array1::zeros(1);
229 single_output[0] = func(x);
230 let h = compute_step_sizes(x, &single_output, &options);
231
232 let result = match options.method.as_str() {
234 "2-point" => {
235 compute_hessian_2point(func.clone(), x, grad_ref, &h, &groups, sparsity, &options)
236 }
237 "3-point" => compute_hessian_3point(func.clone(), x, &h, &groups, sparsity, &options),
238 "cs" => compute_hessian_complex_step(func.clone(), x, &h, &groups, sparsity, &options),
239 _ => Err(OptimizeError::ComputationError(format!(
240 "Unknown method: {}",
241 options.method
242 ))),
243 }?;
244
245 fill_symmetric_hessian(&result)
247}
248
249fn compute_hessian_complex_step<F>(
254 func: F,
255 x: &ArrayView1<f64>,
256 h: &Array1<f64>,
257 groups: &[usize],
258 sparsity: &CsrArray<f64>,
259 options: &SparseFiniteDiffOptions,
260) -> Result<CsrArray<f64>, OptimizeError>
261where
262 F: Fn(&ArrayView1<f64>) -> f64 + Sync + Clone,
263{
264 let (m, n) = sparsity.shape();
265 if m != n {
266 return Err(OptimizeError::ComputationError(
267 "Hessian must be square".to_string(),
268 ));
269 }
270
271 let num_groups = *groups.iter().max().unwrap_or(&0) + 1;
272
273 let mut hess_data = Vec::new();
275 let mut hess_rows = Vec::new();
276 let mut hess_cols = Vec::new();
277
278 let f0 = func(x);
280
281 for group_id in 0..num_groups {
283 let group_indices: Vec<usize> = groups
285 .iter()
286 .enumerate()
287 .filter_map(|(i, &g)| if g == group_id { Some(i) } else { None })
288 .collect();
289
290 if group_indices.is_empty() {
291 continue;
292 }
293
294 for &i in &group_indices {
296 let mut x_h = x.to_owned();
298 let mut x_2h = x.to_owned();
299 let mut x_3h = x.to_owned();
300 let mut x_minus_h = x.to_owned();
301 let mut x_minus_2h = x.to_owned();
302 let mut x_minus_3h = x.to_owned();
303
304 x_h[i] = x[i] + h[i];
305 x_2h[i] = x[i] + 2.0 * h[i];
306 x_3h[i] = x[i] + 3.0 * h[i];
307 x_minus_h[i] = x[i] - h[i];
308 x_minus_2h[i] = x[i] - 2.0 * h[i];
309 x_minus_3h[i] = x[i] - 3.0 * h[i];
310
311 let f_h = func(&x_h.view());
313 let f_2h = func(&x_2h.view());
314 let f_3h = func(&x_3h.view());
315 let f_minus_h = func(&x_minus_h.view());
316 let f_minus_2h = func(&x_minus_2h.view());
317 let f_minus_3h = func(&x_minus_3h.view());
318
319 let d2f_dx2 = (f_minus_3h - 6.0 * f_minus_2h + 15.0 * f_minus_h - 20.0 * f0
322 + 15.0 * f_h
323 - 6.0 * f_2h
324 + f_3h)
325 / (6.0 * h[i] * h[i]);
326
327 hess_rows.push(i);
328 hess_cols.push(i);
329 hess_data.push(d2f_dx2);
330 }
331
332 let parallel = options.parallel.as_ref();
334 let _f0_val = f0; let x_ref = x; let h_ref = h; let func_ref = &func; let group_indices_ref = &group_indices; let processor = move |(i, j): (usize, usize)| {
341 if i == j || !group_indices_ref.contains(&i) || !group_indices_ref.contains(&j) {
342 return None;
343 }
344
345 let mut x_i_plus = x_ref.to_owned();
348 let mut x_i_minus = x_ref.to_owned();
349 let mut x_j_plus = x_ref.to_owned();
350 let mut x_j_minus = x_ref.to_owned();
351 let mut x_ij_plus_plus = x_ref.to_owned();
352 let mut x_ij_plus_minus = x_ref.to_owned();
353 let mut x_ij_minus_plus = x_ref.to_owned();
354 let mut x_ij_minus_minus = x_ref.to_owned();
355
356 x_i_plus[i] = x_ref[i] + h_ref[i];
357 x_i_minus[i] = x_ref[i] - h_ref[i];
358 x_j_plus[j] = x_ref[j] + h_ref[j];
359 x_j_minus[j] = x_ref[j] - h_ref[j];
360
361 x_ij_plus_plus[i] = x_ref[i] + h_ref[i];
362 x_ij_plus_plus[j] = x_ref[j] + h_ref[j];
363
364 x_ij_plus_minus[i] = x_ref[i] + h_ref[i];
365 x_ij_plus_minus[j] = x_ref[j] - h_ref[j];
366
367 x_ij_minus_plus[i] = x_ref[i] - h_ref[i];
368 x_ij_minus_plus[j] = x_ref[j] + h_ref[j];
369
370 x_ij_minus_minus[i] = x_ref[i] - h_ref[i];
371 x_ij_minus_minus[j] = x_ref[j] - h_ref[j];
372
373 let f_i_plus = func_ref(&x_i_plus.view());
375 let f_i_minus = func_ref(&x_i_minus.view());
376 let f_j_plus = func_ref(&x_j_plus.view());
377 let f_j_minus = func_ref(&x_j_minus.view());
378 let f_ij_plus_plus = func_ref(&x_ij_plus_plus.view());
379 let f_ij_plus_minus = func_ref(&x_ij_plus_minus.view());
380 let f_ij_minus_plus = func_ref(&x_ij_minus_plus.view());
381 let f_ij_minus_minus = func_ref(&x_ij_minus_minus.view());
382
383 let d2f_dxdy = (f_ij_plus_plus - f_ij_plus_minus - f_ij_minus_plus + f_ij_minus_minus
386 - 2.0 * (f_i_plus - f_i_minus - f_j_plus + f_j_minus)
387 + 4.0 * _f0_val)
388 / (4.0 * h_ref[i] * h_ref[j]);
389
390 Some((i, j, d2f_dxdy))
391 };
392
393 let mut index_pairs = Vec::new();
395 for i in 0..n {
396 if !group_indices.contains(&i) {
397 continue;
398 }
399
400 let cols = get_nonzero_cols_in_row(sparsity, i);
402 for &j in &cols {
403 if i != j && group_indices.contains(&j) {
404 index_pairs.push((i, j));
405 }
406 }
407 }
408
409 let derivatives: Vec<(usize, usize, f64)> = if let Some(par_opts) = parallel {
411 if index_pairs.len() >= par_opts.min_parallel_size && par_opts.parallel_evaluations {
412 index_pairs
413 .par_iter()
414 .filter_map(|&(i, j)| processor((i, j)))
415 .collect()
416 } else {
417 index_pairs
418 .iter()
419 .filter_map(|&(i, j)| processor((i, j)))
420 .collect()
421 }
422 } else {
423 index_pairs
424 .iter()
425 .filter_map(|&(i, j)| processor((i, j)))
426 .collect()
427 };
428
429 for (row, col, deriv) in derivatives {
431 hess_rows.push(row);
432 hess_cols.push(col);
433 hess_data.push(deriv);
434 }
435 }
436
437 CsrArray::from_triplets(&hess_rows, &hess_cols, &hess_data, (n, n), false)
439 .map_err(|e| OptimizeError::ComputationError(format!("Error creating Hessian: {}", e)))
440}
441
442fn get_nonzero_cols_in_row(matrix: &CsrArray<f64>, row: usize) -> Vec<usize> {
444 let (rows, cols) = matrix.shape();
445 if row >= rows {
446 return Vec::new();
447 }
448
449 let mut nonzero_cols = Vec::new();
450 for col in 0..cols {
451 if !matrix.get(row, col).is_zero() {
452 nonzero_cols.push(col);
453 }
454 }
455
456 nonzero_cols
457}
458
459fn determine_column_groups(
464 sparsity: &CsrArray<f64>,
465 max_group_size: usize,
466 seed: Option<u64>,
467) -> Vec<usize> {
468 let (m, n) = sparsity.shape();
469
470 if n <= 10 {
472 return (0..n).collect();
473 }
474
475 let column_indices: Vec<Vec<usize>> = (0..n)
478 .map(|col| {
479 let mut rows = Vec::new();
480 for row in 0..m {
481 let row_cols = get_nonzero_cols_in_row(sparsity, row);
482 if row_cols.contains(&col) {
483 rows.push(row);
484 }
485 }
486 rows
487 })
488 .collect();
489
490 let mut conflict_graph = vec![Vec::with_capacity(n / 2); n];
492
493 if n > 1000 && m > 1000 {
495 let conflict_lists: Vec<Vec<(usize, usize)>> = (0..n)
496 .into_par_iter()
497 .map(|i| {
498 let mut conflicts = Vec::new();
499 for j in (i + 1)..n {
500 if !column_indices[i].is_empty() && !column_indices[j].is_empty() {
502 let mut has_conflict = false;
503
504 let mut row_set = vec![false; m];
506 for &row in &column_indices[i] {
507 row_set[row] = true;
508 }
509
510 for &row in &column_indices[j] {
511 if row_set[row] {
512 has_conflict = true;
513 break;
514 }
515 }
516
517 if has_conflict {
518 conflicts.push((i, j));
519 }
520 }
521 }
522 conflicts
523 })
524 .collect();
525
526 for conflict_list in conflict_lists {
528 for (i, j) in conflict_list {
529 conflict_graph[i].push(j);
530 conflict_graph[j].push(i);
531 }
532 }
533 } else {
534 for i in 0..n {
536 for j in (i + 1)..n {
537 if !column_indices[i].is_empty() && !column_indices[j].is_empty() {
539 let mut has_conflict = false;
540
541 for &row_i in &column_indices[i] {
542 if column_indices[j].contains(&row_i) {
543 has_conflict = true;
544 break;
545 }
546 }
547
548 if has_conflict {
549 conflict_graph[i].push(j);
550 conflict_graph[j].push(i);
551 }
552 }
553 }
554 }
555 }
556
557 let mut rng = if let Some(seed) = seed {
559 StdRng::seed_from_u64(seed)
560 } else {
561 StdRng::seed_from_u64(rand::random())
562 };
563
564 let mut vertices_by_degree: Vec<(usize, usize)> = conflict_graph
566 .iter()
567 .enumerate()
568 .map(|(idx, neighbors)| (idx, neighbors.len()))
569 .collect();
570
571 vertices_by_degree.shuffle(&mut rng);
573
574 vertices_by_degree.sort_by(|a, b| b.1.cmp(&a.1));
576
577 let mut colors = vec![usize::MAX; n]; let mut color_count = 0;
580
581 for (vertex, _) in &vertices_by_degree {
583 if colors[*vertex] == usize::MAX {
584 let mut available_colors = vec![true; color_count + 1];
588 for &neighbor in &conflict_graph[*vertex] {
589 if colors[neighbor] != usize::MAX && colors[neighbor] < available_colors.len() {
590 available_colors[colors[neighbor]] = false;
591 }
592 }
593
594 let color = available_colors
596 .iter()
597 .position(|&available| available)
598 .unwrap_or_else(|| {
599 color_count += 1;
601 color_count - 1
602 });
603
604 colors[*vertex] = color;
605
606 for (other_vertex, _) in &vertices_by_degree {
608 if *other_vertex == *vertex || colors[*other_vertex] != usize::MAX {
609 continue;
610 }
611
612 let mut can_use_color = true;
614 for &neighbor in &conflict_graph[*other_vertex] {
615 if colors[neighbor] == color {
616 can_use_color = false;
617 break;
618 }
619 }
620
621 if can_use_color {
622 colors[*other_vertex] = color;
623 }
624 }
625 }
626 }
627
628 let mut color_map = std::collections::HashMap::new();
630 let mut new_colors = vec![0; n];
631 let mut next_color = 0;
632
633 for (i, &color) in colors.iter().enumerate() {
634 if let std::collections::hash_map::Entry::Vacant(e) = color_map.entry(color) {
635 e.insert(next_color);
636 next_color += 1;
637 }
638 new_colors[i] = *color_map.get(&color).unwrap();
639 }
640
641 let actual_color_count = color_map.len();
643 if actual_color_count > max_group_size {
644 let colors_per_group = actual_color_count.div_ceil(max_group_size);
646 for color in &mut new_colors {
647 *color /= colors_per_group;
648 }
649 }
650
651 new_colors
652}
653
654fn compute_step_sizes(
656 x: &ArrayView1<f64>,
657 _f0: &Array1<f64>,
658 options: &SparseFiniteDiffOptions,
659) -> Array1<f64> {
660 let n = x.len();
661 let mut h = Array1::zeros(n);
662
663 let default_rel_step = match options.method.as_str() {
665 "2-point" => 1e-8,
666 "3-point" => 1e-5,
667 "cs" => 1e-8,
668 _ => 1e-8,
669 };
670
671 let rel_step = options.rel_step.unwrap_or(default_rel_step);
672
673 if let Some(abs_step) = options.abs_step {
674 for i in 0..n {
676 h[i] = abs_step;
677 }
678 } else {
679 for i in 0..n {
681 let xi_abs = x[i].abs();
682 h[i] = if xi_abs > 0.0 {
683 xi_abs * rel_step
684 } else {
685 rel_step
686 };
687 }
688 }
689
690 if let Some(ref bounds) = options.bounds {
692 for i in 0..n.min(bounds.len()) {
693 let (lb, ub) = bounds[i];
694
695 if x[i] + h[i] > ub {
696 h[i] = -h[i]; }
698
699 if x[i] + h[i] < lb {
700 h[i] = (ub - lb) / 20.0; if x[i] + h[i] > ub {
705 h[i] = (ub - x[i]) / 2.0;
706 }
707 if x[i] - h[i] < lb {
708 h[i] = (x[i] - lb) / 2.0;
709 }
710 }
711 }
712 }
713
714 h
715}
716
717fn compute_jacobian_2point<F>(
719 func: F,
720 x: &ArrayView1<f64>,
721 f0: &Array1<f64>,
722 h: &Array1<f64>,
723 groups: &[usize],
724 sparsity: &CsrArray<f64>,
725 options: &SparseFiniteDiffOptions,
726) -> Result<CsrArray<f64>, OptimizeError>
727where
728 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
729{
730 let (m, n) = sparsity.shape();
731 let num_groups = *groups.iter().max().unwrap_or(&0) + 1;
732
733 let mut jac_data = Vec::new();
735 let mut jac_rows = Vec::new();
736 let mut jac_cols = Vec::new();
737
738 for group_id in 0..num_groups {
740 let group_indices: Vec<usize> = groups
742 .iter()
743 .enumerate()
744 .filter_map(|(i, &g)| if g == group_id { Some(i) } else { None })
745 .collect();
746
747 if group_indices.is_empty() {
748 continue;
749 }
750
751 let mut x_perturbed = x.to_owned();
753 for &idx in &group_indices {
754 x_perturbed[idx] = x[idx] + h[idx];
755 }
756
757 let f_perturbed = func(&x_perturbed.view());
759
760 let parallel = options.parallel.as_ref();
762 let f_perturbed = &f_perturbed; let group_indices = &group_indices; let processor = move |(row, col_indices): (usize, Vec<usize>)| {
769 let diff = f_perturbed[row] - f0[row];
770
771 col_indices
773 .iter()
774 .filter(|&&col| group_indices.contains(&col))
775 .map(move |&col| {
776 let deriv = diff / h[col];
777 (row, col, deriv)
778 })
779 .collect::<Vec<_>>()
780 };
781
782 let row_col_pairs: Vec<(usize, Vec<usize>)> = (0..m)
784 .map(|row| {
785 let cols = get_nonzero_cols_in_row(sparsity, row);
786 (row, cols)
787 })
788 .collect();
789
790 let derivatives: Vec<(usize, usize, f64)> = if let Some(par_opts) = parallel {
792 if row_col_pairs.len() >= par_opts.min_parallel_size && par_opts.parallel_evaluations {
793 row_col_pairs
794 .par_iter()
795 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
796 .collect()
797 } else {
798 row_col_pairs
799 .iter()
800 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
801 .collect()
802 }
803 } else {
804 row_col_pairs
805 .iter()
806 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
807 .collect()
808 };
809
810 for (row, col, deriv) in derivatives {
812 jac_rows.push(row);
813 jac_cols.push(col);
814 jac_data.push(deriv);
815 }
816 }
817
818 CsrArray::from_triplets(&jac_rows, &jac_cols, &jac_data, (m, n), false)
820 .map_err(|e| OptimizeError::ComputationError(format!("Error creating Jacobian: {}", e)))
821}
822
823fn compute_jacobian_3point<F>(
825 func: F,
826 x: &ArrayView1<f64>,
827 _f0: &Array1<f64>,
828 h: &Array1<f64>,
829 groups: &[usize],
830 sparsity: &CsrArray<f64>,
831 options: &SparseFiniteDiffOptions,
832) -> Result<CsrArray<f64>, OptimizeError>
833where
834 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
835{
836 let (m, n) = sparsity.shape();
837 let num_groups = *groups.iter().max().unwrap_or(&0) + 1;
838
839 let mut jac_data = Vec::new();
841 let mut jac_rows = Vec::new();
842 let mut jac_cols = Vec::new();
843
844 for group_id in 0..num_groups {
846 let group_indices: Vec<usize> = groups
848 .iter()
849 .enumerate()
850 .filter_map(|(i, &g)| if g == group_id { Some(i) } else { None })
851 .collect();
852
853 if group_indices.is_empty() {
854 continue;
855 }
856
857 let mut x_plus = x.to_owned();
859 let mut x_minus = x.to_owned();
860
861 for &idx in &group_indices {
862 x_plus[idx] = x[idx] + h[idx];
863 x_minus[idx] = x[idx] - h[idx];
864 }
865
866 let f_plus = func(&x_plus.view());
868 let f_minus = func(&x_minus.view());
869
870 let parallel = options.parallel.as_ref();
872 let f_plus = &f_plus; let f_minus = &f_minus; let group_indices = &group_indices; let processor = move |(row, col_indices): (usize, Vec<usize>)| {
879 col_indices
881 .iter()
882 .filter(|&&col| group_indices.contains(&col))
883 .map(move |&col| {
884 let deriv = (f_plus[row] - f_minus[row]) / (2.0 * h[col]);
885 (row, col, deriv)
886 })
887 .collect::<Vec<_>>()
888 };
889
890 let row_col_pairs: Vec<(usize, Vec<usize>)> = (0..m)
892 .map(|row| {
893 let cols = get_nonzero_cols_in_row(sparsity, row);
894 (row, cols)
895 })
896 .collect();
897
898 let derivatives: Vec<(usize, usize, f64)> = if let Some(par_opts) = parallel {
900 if row_col_pairs.len() >= par_opts.min_parallel_size && par_opts.parallel_evaluations {
901 row_col_pairs
902 .par_iter()
903 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
904 .collect()
905 } else {
906 row_col_pairs
907 .iter()
908 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
909 .collect()
910 }
911 } else {
912 row_col_pairs
913 .iter()
914 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
915 .collect()
916 };
917
918 for (row, col, deriv) in derivatives {
920 jac_rows.push(row);
921 jac_cols.push(col);
922 jac_data.push(deriv);
923 }
924 }
925
926 CsrArray::from_triplets(&jac_rows, &jac_cols, &jac_data, (m, n), false)
928 .map_err(|e| OptimizeError::ComputationError(format!("Error creating Jacobian: {}", e)))
929}
930
931fn compute_jacobian_complex_step<F>(
937 func: F,
938 x: &ArrayView1<f64>,
939 _f0: &Array1<f64>,
940 h: &Array1<f64>,
941 groups: &[usize],
942 sparsity: &CsrArray<f64>,
943 options: &SparseFiniteDiffOptions,
944) -> Result<CsrArray<f64>, OptimizeError>
945where
946 F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
947{
948 let (m, n) = sparsity.shape();
949 let num_groups = *groups.iter().max().unwrap_or(&0) + 1;
950
951 let mut jac_data = Vec::new();
953 let mut jac_rows = Vec::new();
954 let mut jac_cols = Vec::new();
955
956 let _f0_owned = func(x);
958
959 for group_id in 0..num_groups {
961 let group_indices: Vec<usize> = groups
963 .iter()
964 .enumerate()
965 .filter_map(|(i, &g)| if g == group_id { Some(i) } else { None })
966 .collect();
967
968 if group_indices.is_empty() {
969 continue;
970 }
971
972 let mut x_h = x.to_owned();
974 let mut x_2h = x.to_owned();
975 let mut x_minus_h = x.to_owned();
976 let mut x_minus_2h = x.to_owned();
977
978 for &idx in &group_indices {
979 x_h[idx] = x[idx] + h[idx];
980 x_2h[idx] = x[idx] + 2.0 * h[idx];
981 x_minus_h[idx] = x[idx] - h[idx];
982 x_minus_2h[idx] = x[idx] - 2.0 * h[idx];
983 }
984
985 let f_h = func(&x_h.view());
987 let f_2h = func(&x_2h.view());
988 let f_minus_h = func(&x_minus_h.view());
989 let f_minus_2h = func(&x_minus_2h.view());
990
991 let parallel = options.parallel.as_ref();
993 let f_h = &f_h; let f_2h = &f_2h; let f_minus_h = &f_minus_h; let f_minus_2h = &f_minus_2h; let group_indices = &group_indices; let processor = move |(row, col_indices): (usize, Vec<usize>)| {
1002 col_indices
1004 .iter()
1005 .filter(|&&col| group_indices.contains(&col))
1006 .map(move |&col| {
1007 let deriv = (-f_2h[row] + 8.0 * f_h[row] - 8.0 * f_minus_h[row]
1010 + f_minus_2h[row])
1011 / (12.0 * h[col]);
1012 (row, col, deriv)
1013 })
1014 .collect::<Vec<_>>()
1015 };
1016
1017 let row_col_pairs: Vec<(usize, Vec<usize>)> = (0..m)
1019 .map(|row| {
1020 let cols = get_nonzero_cols_in_row(sparsity, row);
1021 (row, cols)
1022 })
1023 .collect();
1024
1025 let derivatives: Vec<(usize, usize, f64)> = if let Some(par_opts) = parallel {
1027 if row_col_pairs.len() >= par_opts.min_parallel_size && par_opts.parallel_evaluations {
1028 row_col_pairs
1029 .par_iter()
1030 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
1031 .collect()
1032 } else {
1033 row_col_pairs
1034 .iter()
1035 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
1036 .collect()
1037 }
1038 } else {
1039 row_col_pairs
1040 .iter()
1041 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
1042 .collect()
1043 };
1044
1045 for (row, col, deriv) in derivatives {
1047 jac_rows.push(row);
1048 jac_cols.push(col);
1049 jac_data.push(deriv);
1050 }
1051 }
1052
1053 CsrArray::from_triplets(&jac_rows, &jac_cols, &jac_data, (m, n), false)
1055 .map_err(|e| OptimizeError::ComputationError(format!("Error creating Jacobian: {}", e)))
1056}
1057
1058fn compute_hessian_2point<F>(
1060 func: F,
1061 x: &ArrayView1<f64>,
1062 grad: &Array1<f64>,
1063 h: &Array1<f64>,
1064 groups: &[usize],
1065 sparsity: &CsrArray<f64>,
1066 options: &SparseFiniteDiffOptions,
1067) -> Result<CsrArray<f64>, OptimizeError>
1068where
1069 F: Fn(&ArrayView1<f64>) -> f64 + Sync + Clone,
1070{
1071 let (m, n) = sparsity.shape();
1072 if m != n {
1073 return Err(OptimizeError::ComputationError(
1074 "Hessian must be square".to_string(),
1075 ));
1076 }
1077
1078 let num_groups = *groups.iter().max().unwrap_or(&0) + 1;
1079
1080 let mut hess_data = Vec::new();
1082 let mut hess_rows = Vec::new();
1083 let mut hess_cols = Vec::new();
1084
1085 for group_id in 0..num_groups {
1087 let group_indices: Vec<usize> = groups
1089 .iter()
1090 .enumerate()
1091 .filter_map(|(i, &g)| if g == group_id { Some(i) } else { None })
1092 .collect();
1093
1094 if group_indices.is_empty() {
1095 continue;
1096 }
1097
1098 let mut x_perturbed = x.to_owned();
1100 for &idx in &group_indices {
1101 x_perturbed[idx] = x[idx] + h[idx];
1102 }
1103
1104 fn calc_gradient<F: Fn(&ArrayView1<f64>) -> f64 + ?Sized>(
1106 func: &F,
1107 x: &ArrayView1<f64>,
1108 ) -> Array1<f64> {
1109 let mut grad = Array1::zeros(x.len());
1110 let eps = 1e-8;
1111
1112 let f0 = func(x);
1113
1114 for i in 0..x.len() {
1115 let mut x_plus = x.to_owned();
1116 x_plus[i] += eps;
1117 let f_plus = func(&x_plus.view());
1118
1119 grad[i] = (f_plus - f0) / eps;
1120 }
1121
1122 grad
1123 }
1124
1125 let grad_perturbed = calc_gradient(&func, &x_perturbed.view());
1127
1128 let parallel = options.parallel.as_ref();
1130 let grad_perturbed = &grad_perturbed; let group_indices = &group_indices; let processor = move |(row, col_indices): (usize, Vec<usize>)| {
1137 col_indices
1139 .iter()
1140 .filter(|&&col| group_indices.contains(&col))
1141 .map(move |&col| {
1142 let deriv = (grad_perturbed[row] - grad[row]) / h[col];
1143 (row, col, deriv)
1144 })
1145 .collect::<Vec<_>>()
1146 };
1147
1148 let row_col_pairs: Vec<(usize, Vec<usize>)> = (0..n)
1150 .map(|row| {
1151 let cols = get_nonzero_cols_in_row(sparsity, row);
1152 (row, cols)
1153 })
1154 .collect();
1155
1156 let derivatives: Vec<(usize, usize, f64)> = if let Some(par_opts) = parallel {
1158 if row_col_pairs.len() >= par_opts.min_parallel_size && par_opts.parallel_evaluations {
1159 row_col_pairs
1160 .par_iter()
1161 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
1162 .collect()
1163 } else {
1164 row_col_pairs
1165 .iter()
1166 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
1167 .collect()
1168 }
1169 } else {
1170 row_col_pairs
1171 .iter()
1172 .flat_map(|&(row, ref cols)| processor((row, cols.clone())))
1173 .collect()
1174 };
1175
1176 for (row, col, deriv) in derivatives {
1178 hess_rows.push(row);
1179 hess_cols.push(col);
1180 hess_data.push(deriv);
1181 }
1182 }
1183
1184 CsrArray::from_triplets(&hess_rows, &hess_cols, &hess_data, (n, n), false)
1186 .map_err(|e| OptimizeError::ComputationError(format!("Error creating Hessian: {}", e)))
1187}
1188
1189fn compute_hessian_3point<F>(
1191 func: F,
1192 x: &ArrayView1<f64>,
1193 h: &Array1<f64>,
1194 groups: &[usize],
1195 sparsity: &CsrArray<f64>,
1196 options: &SparseFiniteDiffOptions,
1197) -> Result<CsrArray<f64>, OptimizeError>
1198where
1199 F: Fn(&ArrayView1<f64>) -> f64 + Sync + Clone,
1200{
1201 let (m, n) = sparsity.shape();
1202 if m != n {
1203 return Err(OptimizeError::ComputationError(
1204 "Hessian must be square".to_string(),
1205 ));
1206 }
1207
1208 let num_groups = *groups.iter().max().unwrap_or(&0) + 1;
1209
1210 let mut hess_data = Vec::new();
1212 let mut hess_rows = Vec::new();
1213 let mut hess_cols = Vec::new();
1214
1215 let f0 = func(x);
1217
1218 for group_id in 0..num_groups {
1220 let group_indices: Vec<usize> = groups
1222 .iter()
1223 .enumerate()
1224 .filter_map(|(i, &g)| if g == group_id { Some(i) } else { None })
1225 .collect();
1226
1227 if group_indices.is_empty() {
1228 continue;
1229 }
1230
1231 for &i in &group_indices {
1233 let mut x_plus = x.to_owned();
1235 let mut x_minus = x.to_owned();
1236 x_plus[i] = x[i] + h[i];
1237 x_minus[i] = x[i] - h[i];
1238
1239 let f_plus = func(&x_plus.view());
1241 let f_minus = func(&x_minus.view());
1242
1243 let d2f_dx2 = (f_plus - 2.0 * f0 + f_minus) / (h[i] * h[i]);
1245
1246 hess_rows.push(i);
1247 hess_cols.push(i);
1248 hess_data.push(d2f_dx2);
1249 }
1250
1251 let parallel = options.parallel.as_ref();
1253 let _f0_val = f0; let x_ref = x; let h_ref = h; let func_ref = &func; let group_indices_ref = &group_indices; let processor = move |(i, j): (usize, usize)| {
1260 if i == j || !group_indices_ref.contains(&j) {
1261 return None;
1262 }
1263
1264 let mut x_plus_plus = x_ref.to_owned();
1266 let mut x_plus_minus = x_ref.to_owned();
1267 let mut x_minus_plus = x_ref.to_owned();
1268 let mut x_minus_minus = x_ref.to_owned();
1269
1270 x_plus_plus[i] = x_ref[i] + h_ref[i];
1271 x_plus_plus[j] = x_ref[j] + h_ref[j];
1272
1273 x_plus_minus[i] = x_ref[i] + h_ref[i];
1274 x_plus_minus[j] = x_ref[j] - h_ref[j];
1275
1276 x_minus_plus[i] = x_ref[i] - h_ref[i];
1277 x_minus_plus[j] = x_ref[j] + h_ref[j];
1278
1279 x_minus_minus[i] = x_ref[i] - h_ref[i];
1280 x_minus_minus[j] = x_ref[j] - h_ref[j];
1281
1282 let f_plus_plus = func_ref(&x_plus_plus.view());
1284 let f_plus_minus = func_ref(&x_plus_minus.view());
1285 let f_minus_plus = func_ref(&x_minus_plus.view());
1286 let f_minus_minus = func_ref(&x_minus_minus.view());
1287
1288 let d2f_dxdy = (f_plus_plus - f_plus_minus - f_minus_plus + f_minus_minus)
1290 / (4.0 * h_ref[i] * h_ref[j]);
1291
1292 Some((i, j, d2f_dxdy))
1293 };
1294
1295 let mut index_pairs = Vec::new();
1297 for i in 0..n {
1298 let cols_i = get_nonzero_cols_in_row(sparsity, i);
1299 for &j in &cols_i {
1300 if i != j && group_indices.contains(&j) {
1301 index_pairs.push((i, j));
1302 }
1303 }
1304 }
1305
1306 let derivatives: Vec<(usize, usize, f64)> = if let Some(par_opts) = parallel {
1308 if index_pairs.len() >= par_opts.min_parallel_size && par_opts.parallel_evaluations {
1309 index_pairs
1310 .par_iter()
1311 .filter_map(|&(i, j)| processor((i, j)))
1312 .collect()
1313 } else {
1314 index_pairs
1315 .iter()
1316 .filter_map(|&(i, j)| processor((i, j)))
1317 .collect()
1318 }
1319 } else {
1320 index_pairs
1321 .iter()
1322 .filter_map(|&(i, j)| processor((i, j)))
1323 .collect()
1324 };
1325
1326 for (row, col, deriv) in derivatives {
1328 hess_rows.push(row);
1329 hess_cols.push(col);
1330 hess_data.push(deriv);
1331 }
1332 }
1333
1334 CsrArray::from_triplets(&hess_rows, &hess_cols, &hess_data, (n, n), false)
1336 .map_err(|e| OptimizeError::ComputationError(format!("Error creating Hessian: {}", e)))
1337}
1338
1339fn fill_symmetric_hessian(upper: &CsrArray<f64>) -> Result<CsrArray<f64>, OptimizeError> {
1341 let (n, _) = upper.shape();
1342
1343 let data = upper.get_data();
1345 let indices = upper.get_indices();
1346 let indptr = upper.get_indptr();
1347
1348 let mut new_data = Vec::new();
1350 let mut new_rows = Vec::new();
1351 let mut new_cols = Vec::new();
1352
1353 for i in 0..n {
1355 let start = indptr[i];
1356 let end = indptr[i + 1];
1357
1358 for k in start..end {
1360 let j = indices[k];
1361 new_data.push(data[k]);
1362 new_rows.push(i);
1363 new_cols.push(j);
1364
1365 if i != j {
1367 new_data.push(data[k]);
1368 new_rows.push(j); new_cols.push(i);
1370 }
1371 }
1372 }
1373
1374 CsrArray::from_triplets(&new_rows, &new_cols, &new_data, (n, n), false).map_err(|e| {
1376 OptimizeError::ComputationError(format!("Error creating symmetric Hessian: {}", e))
1377 })
1378}
1379
1380#[cfg(test)]
1381mod tests {
1382 use super::*;
1383 use ndarray::array;
1384
1385 fn sphere(x: &ArrayView1<f64>) -> f64 {
1387 x.iter().map(|&xi| xi * xi).sum()
1388 }
1389
1390 fn multi_output(x: &ArrayView1<f64>) -> Array1<f64> {
1392 let mut result = Array1::zeros(2);
1393 result[0] = x[0].powi(2) + x[1];
1394 result[1] = x[0] * x[1];
1395 result
1396 }
1397
1398 fn himmelblau(x: &ArrayView1<f64>) -> f64 {
1400 (x[0].powi(2) + x[1] - 11.0).powi(2) + (x[0] + x[1].powi(2) - 7.0).powi(2)
1401 }
1402
1403 #[test]
1404 fn test_sparse_jacobian_dense() {
1405 let x = array![1.0, 2.0];
1407
1408 let jac = sparse_jacobian(multi_output, &x.view(), None, None, None).unwrap();
1410
1411 let expected = array![[2.0, 1.0], [2.0, 1.0]];
1415
1416 let jac_dense = jac.to_array();
1418
1419 assert_eq!(jac_dense.shape(), [2, 2]);
1421
1422 for i in 0..2 {
1424 for j in 0..2 {
1425 assert!(
1426 (jac_dense[[i, j]] - expected[[i, j]]).abs() < 1e-5,
1427 "Jacobian element [{}, {}] = {} doesn't match expected {}",
1428 i,
1429 j,
1430 jac_dense[[i, j]],
1431 expected[[i, j]]
1432 );
1433 }
1434 }
1435 }
1436
1437 #[test]
1438 fn test_sparse_jacobian_higher_order() {
1439 let x = array![1.0, 2.0];
1441
1442 let mut options = SparseFiniteDiffOptions::default();
1444 options.method = "cs".to_string();
1445
1446 let jac = sparse_jacobian(multi_output, &x.view(), None, None, Some(options)).unwrap();
1448
1449 let expected = array![[2.0, 1.0], [2.0, 1.0]];
1453
1454 let jac_dense = jac.to_array();
1456
1457 assert_eq!(jac_dense.shape(), [2, 2]);
1459
1460 for i in 0..2 {
1462 for j in 0..2 {
1463 assert!(
1464 (jac_dense[[i, j]] - expected[[i, j]]).abs() < 1e-5,
1465 "Jacobian element [{}, {}] = {} doesn't match expected {}",
1466 i,
1467 j,
1468 jac_dense[[i, j]],
1469 expected[[i, j]]
1470 );
1471 }
1472 }
1473 }
1474
1475 #[test]
1476 fn test_sparse_hessian_sphere() {
1477 let x = array![1.0, 2.0];
1479
1480 let mut options = SparseFiniteDiffOptions::default();
1482 options.method = "3-point".to_string();
1483 let hess = sparse_hessian(sphere, &x.view(), None, None, Some(options)).unwrap();
1484
1485 let expected = array![[2.0, 0.0], [0.0, 2.0]];
1487
1488 let hess_dense = hess.to_array();
1490
1491 assert_eq!(hess_dense.shape(), [2, 2]);
1493
1494 for i in 0..2 {
1496 for j in 0..2 {
1497 assert!(
1498 (hess_dense[[i, j]] - expected[[i, j]]).abs() < 1e-5,
1499 "Hessian element [{}, {}] = {} doesn't match expected {}",
1500 i,
1501 j,
1502 hess_dense[[i, j]],
1503 expected[[i, j]]
1504 );
1505 }
1506 }
1507 }
1508
1509 #[test]
1510 fn test_sparse_hessian_higher_order() {
1511 let x = array![1.0, 2.0];
1513
1514 let mut options = SparseFiniteDiffOptions::default();
1516 options.method = "3-point".to_string();
1517
1518 let hess = sparse_hessian(sphere, &x.view(), None, None, Some(options)).unwrap();
1520
1521 let expected = array![[2.0, 0.0], [0.0, 2.0]];
1523
1524 let hess_dense = hess.to_array();
1526
1527 assert_eq!(hess_dense.shape(), [2, 2]);
1529
1530 for i in 0..2 {
1532 for j in 0..2 {
1533 assert!(
1534 (hess_dense[[i, j]] - expected[[i, j]]).abs() < 1e-5,
1535 "Hessian element [{}, {}] = {} doesn't match expected {}",
1536 i,
1537 j,
1538 hess_dense[[i, j]],
1539 expected[[i, j]]
1540 );
1541 }
1542 }
1543 }
1544
1545 #[test]
1546 fn test_sparse_hessian_rosenbrock() {
1547 let rosenbrock = |x: &ArrayView1<f64>| {
1549 let a = 1.0;
1550 let b = 100.0;
1551 (a - x[0]).powi(2) + b * (x[1] - x[0].powi(2)).powi(2)
1552 };
1553
1554 let x = array![1.0, 1.0]; let hess = sparse_hessian(rosenbrock, &x.view(), None, None, None).unwrap();
1559
1560 let expected = array![[802.0, -400.0], [-400.0, 200.0]];
1562
1563 let hess_dense = hess.to_array();
1565
1566 assert_eq!(hess_dense.shape(), [2, 2]);
1568
1569 for i in 0..2 {
1571 for j in 0..2 {
1572 assert!(
1573 (hess_dense[[i, j]] - expected[[i, j]]).abs() < 5.0,
1574 "Hessian element [{}, {}] = {} doesn't match expected {}",
1575 i,
1576 j,
1577 hess_dense[[i, j]],
1578 expected[[i, j]]
1579 );
1580 }
1581 }
1582 }
1583
1584 #[test]
1585 fn test_column_grouping() {
1586 let indices_row = vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 4];
1588 let indices_col = vec![0, 1, 1, 2, 2, 3, 3, 4, 0, 4];
1589 let data = vec![1.0; 10];
1590 let n = 5;
1591
1592 let sparsity =
1593 CsrArray::from_triplets(&indices_row, &indices_col, &data, (n, n), false).unwrap();
1594
1595 let groups = determine_column_groups(&sparsity, 10, Some(42));
1597
1598 for row in 0..n {
1600 let row_cols = get_nonzero_cols_in_row(&sparsity, row);
1601 for (i, &col_i) in row_cols.iter().enumerate() {
1602 for (j, &col_j) in row_cols.iter().enumerate() {
1603 if i != j {
1604 assert_ne!(
1605 groups[col_i], groups[col_j],
1606 "Columns {} and {} share row {} but have same group {} and {}",
1607 col_i, col_j, row, groups[col_i], groups[col_j]
1608 );
1609 }
1610 }
1611 }
1612 }
1613 }
1614
1615 #[test]
1616 fn test_large_sparse_jacobian() {
1617 fn tridiagonal_func(x: &ArrayView1<f64>) -> Array1<f64> {
1619 let n = x.len();
1620 let mut result = Array1::zeros(n);
1621
1622 for i in 0..n {
1623 if i > 0 {
1624 result[i] += x[i - 1]; }
1626 result[i] += 2.0 * x[i]; if i < n - 1 {
1628 result[i] += x[i + 1]; }
1630 }
1631
1632 result
1633 }
1634
1635 let n = 100;
1637 let x = Array1::from_vec((0..n).map(|i| i as f64).collect());
1638
1639 let mut indices_row = Vec::new();
1641 let mut indices_col = Vec::new();
1642 let mut data = Vec::new();
1643
1644 for i in 0..n {
1645 if i > 0 {
1646 indices_row.push(i);
1647 indices_col.push(i - 1);
1648 data.push(1.0);
1649 }
1650
1651 indices_row.push(i);
1652 indices_col.push(i);
1653 data.push(1.0);
1654
1655 if i < n - 1 {
1656 indices_row.push(i);
1657 indices_col.push(i + 1);
1658 data.push(1.0);
1659 }
1660 }
1661
1662 let sparsity =
1663 CsrArray::from_triplets(&indices_row, &indices_col, &data, (n, n), false).unwrap();
1664
1665 let mut options = SparseFiniteDiffOptions::default();
1667 options.parallel = Some(ParallelOptions::default());
1668
1669 let jac = sparse_jacobian(
1671 tridiagonal_func,
1672 &x.view(),
1673 None,
1674 Some(&sparsity),
1675 Some(options),
1676 )
1677 .unwrap();
1678
1679 assert_eq!(jac.shape(), (n, n));
1681
1682 for i in 0..n {
1684 let row_cols = get_nonzero_cols_in_row(&jac, i);
1685
1686 if i > 0 {
1687 assert!(
1688 row_cols.contains(&(i - 1)),
1689 "Lower diagonal missing at row {}",
1690 i
1691 );
1692 }
1693
1694 assert!(row_cols.contains(&i), "Diagonal missing at row {}", i);
1695
1696 if i < n - 1 {
1697 assert!(
1698 row_cols.contains(&(i + 1)),
1699 "Upper diagonal missing at row {}",
1700 i
1701 );
1702 }
1703
1704 assert!(row_cols.len() <= 3, "Row {} has too many non-zeros", i);
1706 }
1707 }
1708
1709 #[test]
1710 fn test_sparse_himmelblau_hessian() {
1711 let critical_points = [
1713 array![3.0, 2.0], array![-2.805118, 3.131312], array![-3.779310, -3.283186], array![3.584428, -1.848126], ];
1718
1719 for (i, x) in critical_points.iter().enumerate() {
1720 let mut options = SparseFiniteDiffOptions::default();
1722 options.method = "3-point".to_string();
1723
1724 let hess = sparse_hessian(himmelblau, &x.view(), None, None, Some(options)).unwrap();
1726
1727 assert_eq!(hess.shape(), (2, 2));
1729
1730 let diag0 = hess.get(0, 0);
1733 let diag1 = hess.get(1, 1);
1734
1735 assert!(diag0 > 0.0, "Diagonal[0,0] not positive at minimum {}", i);
1736 assert!(diag1 > 0.0, "Diagonal[1,1] not positive at minimum {}", i);
1737
1738 let det = diag0 * diag1 - hess.get(0, 1) * hess.get(1, 0);
1740 assert!(
1741 det > 0.0,
1742 "Hessian not positive definite at minimum {}, det={}",
1743 i,
1744 det
1745 );
1746 }
1747 }
1748}