scirs2_optimize/
sparse_numdiff.rs

1//! Sparse numerical differentiation for large-scale optimization
2//!
3//! This module provides functions for computing sparse Jacobians and Hessians
4//! using finite differences, designed for large-scale optimization problems.
5
6use 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/// Options for sparse numerical differentiation
17#[derive(Debug, Clone)]
18pub struct SparseFiniteDiffOptions {
19    /// Method to use for finite differences ('2-point', '3-point', 'cs')
20    pub method: String,
21    /// Relative step size (if None, determined automatically)
22    pub rel_step: Option<f64>,
23    /// Absolute step size (if None, determined automatically)
24    pub abs_step: Option<f64>,
25    /// Bounds on the variables
26    pub bounds: Option<Vec<(f64, f64)>>,
27    /// Parallel computation options
28    pub parallel: Option<ParallelOptions>,
29    /// Random seed for coloring algorithm
30    pub seed: Option<u64>,
31    /// Maximum number of columns to group together
32    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
49/// Computes a sparse Jacobian matrix using finite differences
50///
51/// # Arguments
52///
53/// * `func` - Function to differentiate, takes ArrayView1<f64> and returns Array1<f64>
54/// * `x` - Point at which to compute the Jacobian
55/// * `f0` - Function value at `x` (if None, computed internally)
56/// * `sparsity_pattern` - Sparse matrix indicating the known sparsity pattern (if None, dense Jacobian)
57/// * `options` - Options for finite differences computation
58///
59/// # Returns
60///
61/// * `CsrArray<f64>` - Sparse Jacobian matrix in CSR format
62///
63pub 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    // Compute f0 if not provided
76    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(); // Input dimension
86    let m = f0_ref.len(); // Output dimension
87
88    // If no sparsity pattern provided, create a dense one
89    let sparsity_owned: CsrArray<f64>;
90    let sparsity = match sparsity_pattern {
91        Some(p) => p,
92        None => {
93            // Create dense sparsity pattern
94            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    // Determine column groups for parallel evaluation using a greedy coloring algorithm
118    let groups = determine_column_groups(sparsity, options.max_group_size, options.seed);
119
120    // Compute step sizes
121    let h = compute_step_sizes(x, f0_ref, &options);
122
123    // Compute the Jacobian based on the method
124    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
135/// Computes a sparse Hessian matrix using finite differences
136///
137/// # Arguments
138///
139/// * `func` - Function to differentiate, takes ArrayView1<f64> and returns f64
140/// * `x` - Point at which to compute the Hessian
141/// * `grad` - Gradient value at `x` (if None, computed internally)
142/// * `sparsity_pattern` - Sparse matrix indicating the known sparsity pattern (if None, dense Hessian)
143/// * `options` - Options for finite differences computation
144///
145/// # Returns
146///
147/// * `CsrArray<f64>` - Sparse Hessian matrix in CSR format
148///
149pub 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    // Create gradient function as a separate function to avoid borrowing issues
163    fn grad_fn<F: Fn(&ArrayView1<f64>) -> f64 + Clone>(
164        func: &F,
165        x: &ArrayView1<f64>,
166    ) -> Array1<f64> {
167        // Create a simple gradient approximation function
168        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    // Compute gradient if not provided
185    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    // If no sparsity pattern provided, create a dense one (or use symmetry)
195    let sparsity_owned: CsrArray<f64>;
196    let sparsity = match sparsity_pattern {
197        Some(p) => p,
198        None => {
199            // For Hessian, we can exploit symmetry and only compute upper triangular part
200            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                    // Upper triangular part only
207                    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    // Determine column groups using a graph coloring algorithm
225    let groups = determine_column_groups(sparsity, options.max_group_size, options.seed);
226
227    // Compute step sizes
228    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    // Compute the Hessian based on the method
233    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 in the lower triangular part using symmetry for a full Hessian
246    fill_symmetric_hessian(&result)
247}
248
249/// Computes Hessian using a higher-order finite difference method
250///
251/// This implements a sixth-order accurate approximation as an alternative to
252/// the complex step method for Hessian calculation.
253fn 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    // Prepare result data structures
274    let mut hess_data = Vec::new();
275    let mut hess_rows = Vec::new();
276    let mut hess_cols = Vec::new();
277
278    // Base function value
279    let f0 = func(x);
280
281    // Process each group of columns
282    for group_id in 0..num_groups {
283        // Determine indices in this group
284        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 Hessian diagonal elements (i,i)
295        for &i in &group_indices {
296            // Create perturbations with multiple step sizes for higher-order accuracy
297            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            // Evaluate function
312            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            // Calculate diagonal element using 6th-order central difference
320            // Formula: (f(x-3h) - 6f(x-2h) + 15f(x-h) - 20f(x) + 15f(x+h) - 6f(x+2h) + f(x+3h)) / (h^2 * 6)
321            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        // For off-diagonal elements (i,j) where i≠j
333        let parallel = options.parallel.as_ref();
334        let _f0_val = f0; // Capture for closure
335        let x_ref = x; // Capture x by reference
336        let h_ref = h; // Capture h by reference
337        let func_ref = &func; // Capture function by reference
338        let group_indices_ref = &group_indices; // Capture group_indices by reference
339
340        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            // Create perturbations for mixed partial derivatives
346            // We'll use a 4th-order method for mixed derivatives
347            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            // Evaluate function at all points
374            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            // Mixed partial derivative using higher-order formula
384            // This is more accurate than the standard formula (f_pp - f_pm - f_mp + f_mm)/(4h_i*h_j)
385            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        // Get all pairs of indices from sparsity pattern
394        let mut index_pairs = Vec::new();
395        for i in 0..n {
396            if !group_indices.contains(&i) {
397                continue;
398            }
399
400            // Get non-zero columns in this row
401            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        // Process index pairs in parallel or sequentially
410        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        // Add derivatives to result
430        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    // Create the sparse Hessian matrix
438    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
442/// Helper function to get nonzero column indices in a row of a sparse matrix
443fn 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
459/// Determines efficient column groupings for parallel finite differencing
460///
461/// Implements a greedy graph coloring algorithm to group columns that can be
462/// perturbed simultaneously based on the sparsity pattern.
463fn 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    // For very small problems, use simple sequential grouping
471    if n <= 10 {
472        return (0..n).collect();
473    }
474
475    // Create a compressed column representation of the sparsity pattern
476    // This will make it easier to determine conflicts between columns
477    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    // Build conflicts graph: columns i and j conflict if they share any rows
491    let mut conflict_graph = vec![Vec::with_capacity(n / 2); n];
492
493    // Use a parallel approach for building the conflict graph for large matrices
494    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                    // Check if columns i and j share any rows
501                    if !column_indices[i].is_empty() && !column_indices[j].is_empty() {
502                        let mut has_conflict = false;
503
504                        // Use a set-like approach for faster intersection check
505                        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        // Combine conflict lists into the conflict graph
527        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 smaller matrices, use a simpler approach
535        for i in 0..n {
536            for j in (i + 1)..n {
537                // Check if columns i and j share any rows
538                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    // Initialize a random number generator for shuffling
558    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    // Sort vertices by degree (number of conflicts) for better coloring
565    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    // Break ties randomly for better coloring
572    vertices_by_degree.shuffle(&mut rng);
573
574    // Sort by degree in descending order (largest degree first)
575    vertices_by_degree.sort_by(|a, b| b.1.cmp(&a.1));
576
577    // Greedy coloring algorithm (Welsh-Powell)
578    let mut colors = vec![usize::MAX; n]; // usize::MAX means uncolored
579    let mut color_count = 0;
580
581    // Process vertices in order of descending degree
582    for (vertex, _) in &vertices_by_degree {
583        if colors[*vertex] == usize::MAX {
584            // Vertex not yet colored
585
586            // Find smallest available color not used by neighbors
587            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            // Find first available color
595            let color = available_colors
596                .iter()
597                .position(|&available| available)
598                .unwrap_or_else(|| {
599                    // All existing colors are used by neighbors, create a new color
600                    color_count += 1;
601                    color_count - 1
602                });
603
604            colors[*vertex] = color;
605
606            // Look for other non-adjacent vertices that can use the same color
607            for (other_vertex, _) in &vertices_by_degree {
608                if *other_vertex == *vertex || colors[*other_vertex] != usize::MAX {
609                    continue;
610                }
611
612                // Check if this vertex is adjacent to any vertex colored with 'color'
613                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    // Ensure colors are numbered from 0 and contiguous
629    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    // Ensure no group is too large
642    let actual_color_count = color_map.len();
643    if actual_color_count > max_group_size {
644        // Combine colors to reduce the number of groups
645        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
654/// Computes appropriate step sizes for finite differences
655fn 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    // Default relative step sizes based on method
664    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        // User specified absolute steps
675        for i in 0..n {
676            h[i] = abs_step;
677        }
678    } else {
679        // Compute relative steps
680        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    // Adjust step sizes for bounds
691    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]; // Use negative step if upper bound is hit
697            }
698
699            if x[i] + h[i] < lb {
700                // If we hit lower bound too, reduce step size
701                h[i] = (ub - lb) / 20.0; // Use a small fraction of the range
702
703                // Make sure we don't exceed bounds
704                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
717/// Computes Jacobian using the 2-point method
718fn 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    // Prepare result data structures
734    let mut jac_data = Vec::new();
735    let mut jac_rows = Vec::new();
736    let mut jac_cols = Vec::new();
737
738    // Process each group of columns
739    for group_id in 0..num_groups {
740        // Determine indices in this group
741        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        // Create perturbation
752        let mut x_perturbed = x.to_owned();
753        for &idx in &group_indices {
754            x_perturbed[idx] = x[idx] + h[idx];
755        }
756
757        // Evaluate the function at the perturbed point
758        let f_perturbed = func(&x_perturbed.view());
759
760        // Calculate the partial derivatives for this group
761        let parallel = options.parallel.as_ref();
762        let f_perturbed = &f_perturbed; // Capture by reference
763                                        // f0 is already captured by reference
764                                        // h is already captured by reference
765        let group_indices = &group_indices; // Capture by reference
766
767        // Use a closure that captures all needed variables by reference to avoid ownership issues
768        let processor = move |(row, col_indices): (usize, Vec<usize>)| {
769            let diff = f_perturbed[row] - f0[row];
770
771            // Calculate derivatives for all columns in this row that belong to the current group
772            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        // Get all affected rows and their column indices from the sparsity pattern
783        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        // Process rows in parallel or sequentially
791        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        // Add derivatives to result
811        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    // Create the sparse Jacobian matrix
819    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
823/// Computes Jacobian using the 3-point method
824fn 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    // Prepare result data structures
840    let mut jac_data = Vec::new();
841    let mut jac_rows = Vec::new();
842    let mut jac_cols = Vec::new();
843
844    // Process each group of columns
845    for group_id in 0..num_groups {
846        // Determine indices in this group
847        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        // Create positive and negative perturbations
858        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        // Evaluate the function at the perturbed points
867        let f_plus = func(&x_plus.view());
868        let f_minus = func(&x_minus.view());
869
870        // Calculate the partial derivatives for this group
871        let parallel = options.parallel.as_ref();
872        let f_plus = &f_plus; // Capture by reference
873        let f_minus = &f_minus; // Capture by reference
874                                // h is already captured by reference
875        let group_indices = &group_indices; // Capture by reference
876
877        // Use a closure that captures all needed variables by reference to avoid ownership issues
878        let processor = move |(row, col_indices): (usize, Vec<usize>)| {
879            // Calculate derivatives for all columns in this row that belong to the current group
880            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        // Get all affected rows and their column indices from the sparsity pattern
891        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        // Process rows in parallel or sequentially
899        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        // Add derivatives to result
919        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    // Create the sparse Jacobian matrix
927    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
931/// Computes Jacobian using the complex step method
932///
933/// This implements a fourth-order accurate approximation as an alternative to
934/// the complex step method, since Rust doesn't natively support automatic complex
935/// number differentiation for arbitrary functions.
936fn 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    // Prepare result data structures
952    let mut jac_data = Vec::new();
953    let mut jac_rows = Vec::new();
954    let mut jac_cols = Vec::new();
955
956    // Compute f0 if needed (but we're actually using _f0 from parameters)
957    let _f0_owned = func(x);
958
959    // Process each group of columns
960    for group_id in 0..num_groups {
961        // Determine indices in this group
962        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        // Create perturbations at different step sizes for higher-order accuracy
973        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        // Evaluate the function at the perturbed points
986        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        // Calculate the partial derivatives using 4th-order central difference formula
992        let parallel = options.parallel.as_ref();
993        let f_h = &f_h; // Capture by reference
994        let f_2h = &f_2h; // Capture by reference
995        let f_minus_h = &f_minus_h; // Capture by reference
996        let f_minus_2h = &f_minus_2h; // Capture by reference
997                                      // h is already captured by reference
998        let group_indices = &group_indices; // Capture by reference
999
1000        // Use a closure that captures all needed variables by reference to avoid ownership issues
1001        let processor = move |(row, col_indices): (usize, Vec<usize>)| {
1002            // Calculate derivatives for all columns in this row that belong to the current group
1003            col_indices
1004                .iter()
1005                .filter(|&&col| group_indices.contains(&col))
1006                .map(move |&col| {
1007                    // Fourth-order accurate central difference formula
1008                    // f'(x) ≈ (-f(x+2h) + 8f(x+h) - 8f(x-h) + f(x-2h)) / (12h)
1009                    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        // Get all affected rows and their column indices from the sparsity pattern
1018        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        // Process rows in parallel or sequentially
1026        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        // Add derivatives to result
1046        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    // Create the sparse Jacobian matrix
1054    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
1058/// Computes Hessian using the 2-point method
1059fn 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    // Prepare result data structures
1081    let mut hess_data = Vec::new();
1082    let mut hess_rows = Vec::new();
1083    let mut hess_cols = Vec::new();
1084
1085    // Process each group of columns
1086    for group_id in 0..num_groups {
1087        // Determine indices in this group
1088        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        // For Hessian, we'll compute the gradient at perturbed points
1099        let mut x_perturbed = x.to_owned();
1100        for &idx in &group_indices {
1101            x_perturbed[idx] = x[idx] + h[idx];
1102        }
1103
1104        // Create a simple gradient approximation function
1105        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        // Evaluate the gradient at the perturbed point
1126        let grad_perturbed = calc_gradient(&func, &x_perturbed.view());
1127
1128        // Calculate the second derivatives for this group
1129        let parallel = options.parallel.as_ref();
1130        let grad_perturbed = &grad_perturbed; // Capture by reference
1131                                              // grad is already captured by reference
1132                                              // h is already captured by reference
1133        let group_indices = &group_indices; // Capture by reference
1134
1135        // Use a closure that captures all needed variables by reference to avoid ownership issues
1136        let processor = move |(row, col_indices): (usize, Vec<usize>)| {
1137            // Calculate derivatives for all columns in this row that belong to the current group
1138            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        // Get all row and column pairs from the sparsity pattern
1149        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        // Process rows in parallel or sequentially
1157        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        // Add derivatives to result
1177        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    // Create the sparse Hessian matrix
1185    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
1189/// Computes Hessian using the 3-point method
1190fn 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    // Prepare result data structures
1211    let mut hess_data = Vec::new();
1212    let mut hess_rows = Vec::new();
1213    let mut hess_cols = Vec::new();
1214
1215    // Base function value
1216    let f0 = func(x);
1217
1218    // Process each group of columns
1219    for group_id in 0..num_groups {
1220        // Determine indices in this group
1221        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 Hessian diagonal elements (i,i)
1232        for &i in &group_indices {
1233            // Create perturbations
1234            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            // Evaluate function
1240            let f_plus = func(&x_plus.view());
1241            let f_minus = func(&x_minus.view());
1242
1243            // Calculate diagonal element using central difference
1244            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        // For off-diagonal elements (i,j) where i≠j
1252        let parallel = options.parallel.as_ref();
1253        let _f0_val = f0; // Capture for closure
1254        let x_ref = x; // Capture x by reference
1255        let h_ref = h; // Capture h by reference
1256        let func_ref = &func; // Capture function by reference
1257        let group_indices_ref = &group_indices; // Capture group_indices by reference
1258
1259        let processor = move |(i, j): (usize, usize)| {
1260            if i == j || !group_indices_ref.contains(&j) {
1261                return None;
1262            }
1263
1264            // Create perturbations for mixed partial derivative
1265            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            // Evaluate function at all points
1283            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            // Mixed partial derivative using central difference
1289            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        // Get all pairs of indices from sparsity pattern
1296        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        // Process index pairs in parallel or sequentially
1307        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        // Add derivatives to result
1327        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    // Create the sparse Hessian matrix
1335    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
1339/// Fills in the lower triangular part of a symmetric matrix
1340fn fill_symmetric_hessian(upper: &CsrArray<f64>) -> Result<CsrArray<f64>, OptimizeError> {
1341    let (n, _) = upper.shape();
1342
1343    // Extract CSR components
1344    let data = upper.get_data();
1345    let indices = upper.get_indices();
1346    let indptr = upper.get_indptr();
1347
1348    // Create new entries for both upper and lower triangular parts
1349    let mut new_data = Vec::new();
1350    let mut new_rows = Vec::new();
1351    let mut new_cols = Vec::new();
1352
1353    // Process each row
1354    for i in 0..n {
1355        let start = indptr[i];
1356        let end = indptr[i + 1];
1357
1358        // Add original entries from the upper triangular part
1359        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            // Add symmetric entry for off-diagonal elements
1366            if i != j {
1367                new_data.push(data[k]);
1368                new_rows.push(j); // Swap i and j
1369                new_cols.push(i);
1370            }
1371        }
1372    }
1373
1374    // Create the full symmetric Hessian
1375    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    /// Test function: sphere function
1386    fn sphere(x: &ArrayView1<f64>) -> f64 {
1387        x.iter().map(|&xi| xi * xi).sum()
1388    }
1389
1390    /// Test function: multivariate output
1391    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    /// Himmelblau function: has multiple local minima
1399    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        // Create a test point
1406        let x = array![1.0, 2.0];
1407
1408        // Compute Jacobian
1409        let jac = sparse_jacobian(multi_output, &x.view(), None, None, None).unwrap();
1410
1411        // Expected Jacobian
1412        // [2*x[0], 1]
1413        // [x[1], x[0]]
1414        let expected = array![[2.0, 1.0], [2.0, 1.0]];
1415
1416        // Compare with expected result
1417        let jac_dense = jac.to_array();
1418
1419        // Check dimensions
1420        assert_eq!(jac_dense.shape(), [2, 2]);
1421
1422        // Check values (with tolerance for numerical accuracy)
1423        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        // Create a test point
1440        let x = array![1.0, 2.0];
1441
1442        // Create options for 4th-order method
1443        let mut options = SparseFiniteDiffOptions::default();
1444        options.method = "cs".to_string();
1445
1446        // Compute Jacobian
1447        let jac = sparse_jacobian(multi_output, &x.view(), None, None, Some(options)).unwrap();
1448
1449        // Expected Jacobian
1450        // [2*x[0], 1]
1451        // [x[1], x[0]]
1452        let expected = array![[2.0, 1.0], [2.0, 1.0]];
1453
1454        // Compare with expected result
1455        let jac_dense = jac.to_array();
1456
1457        // Check dimensions
1458        assert_eq!(jac_dense.shape(), [2, 2]);
1459
1460        // Check values (with tolerance for numerical accuracy)
1461        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        // Create a test point
1478        let x = array![1.0, 2.0];
1479
1480        // Compute Hessian using 3-point method
1481        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        // Expected Hessian for sphere function is 2*I
1486        let expected = array![[2.0, 0.0], [0.0, 2.0]];
1487
1488        // Compare with expected result
1489        let hess_dense = hess.to_array();
1490
1491        // Check dimensions
1492        assert_eq!(hess_dense.shape(), [2, 2]);
1493
1494        // Check values (with tolerance for numerical accuracy)
1495        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        // Create a test point
1512        let x = array![1.0, 2.0];
1513
1514        // Create options for 3-point method (since cs has issues)
1515        let mut options = SparseFiniteDiffOptions::default();
1516        options.method = "3-point".to_string();
1517
1518        // Compute Hessian
1519        let hess = sparse_hessian(sphere, &x.view(), None, None, Some(options)).unwrap();
1520
1521        // Expected Hessian for sphere function is 2*I
1522        let expected = array![[2.0, 0.0], [0.0, 2.0]];
1523
1524        // Compare with expected result
1525        let hess_dense = hess.to_array();
1526
1527        // Check dimensions
1528        assert_eq!(hess_dense.shape(), [2, 2]);
1529
1530        // Check values (with tolerance for numerical accuracy)
1531        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        // Rosenbrock function
1548        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        // Create a test point
1555        let x = array![1.0, 1.0]; // At minimum
1556
1557        // Compute Hessian
1558        let hess = sparse_hessian(rosenbrock, &x.view(), None, None, None).unwrap();
1559
1560        // Expected Hessian at (1,1) - not a diagonal matrix
1561        let expected = array![[802.0, -400.0], [-400.0, 200.0]];
1562
1563        // Compare with expected result
1564        let hess_dense = hess.to_array();
1565
1566        // Check dimensions
1567        assert_eq!(hess_dense.shape(), [2, 2]);
1568
1569        // Check values (with higher tolerance due to numerical issues with Rosenbrock)
1570        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        // Create a simple sparsity pattern
1587        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        // Compute groups
1596        let groups = determine_column_groups(&sparsity, 10, Some(42));
1597
1598        // Verify that no two adjacent columns have the same group
1599        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        // Create a large sparse function (tridiagonal)
1618        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]; // Lower diagonal
1625                }
1626                result[i] += 2.0 * x[i]; // Diagonal
1627                if i < n - 1 {
1628                    result[i] += x[i + 1]; // Upper diagonal
1629                }
1630            }
1631
1632            result
1633        }
1634
1635        // Create test point (size 100 for a large sparse system)
1636        let n = 100;
1637        let x = Array1::from_vec((0..n).map(|i| i as f64).collect());
1638
1639        // Create sparsity pattern (tridiagonal)
1640        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        // Create options with parallel computation enabled
1666        let mut options = SparseFiniteDiffOptions::default();
1667        options.parallel = Some(ParallelOptions::default());
1668
1669        // Compute Jacobian
1670        let jac = sparse_jacobian(
1671            tridiagonal_func,
1672            &x.view(),
1673            None,
1674            Some(&sparsity),
1675            Some(options),
1676        )
1677        .unwrap();
1678
1679        // Verify dimensions
1680        assert_eq!(jac.shape(), (n, n));
1681
1682        // Verify sparsity pattern is preserved (should be tridiagonal)
1683        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            // Should not have any other non-zeros
1705            assert!(row_cols.len() <= 3, "Row {} has too many non-zeros", i);
1706        }
1707    }
1708
1709    #[test]
1710    fn test_sparse_himmelblau_hessian() {
1711        // Test points at different critical points of Himmelblau function
1712        let critical_points = [
1713            array![3.0, 2.0],             // Minimum
1714            array![-2.805118, 3.131312],  // Minimum
1715            array![-3.779310, -3.283186], // Minimum
1716            array![3.584428, -1.848126],  // Minimum
1717        ];
1718
1719        for (i, x) in critical_points.iter().enumerate() {
1720            // Create options with 3-point method
1721            let mut options = SparseFiniteDiffOptions::default();
1722            options.method = "3-point".to_string();
1723
1724            // Compute Hessian
1725            let hess = sparse_hessian(himmelblau, &x.view(), None, None, Some(options)).unwrap();
1726
1727            // Verify dimensions
1728            assert_eq!(hess.shape(), (2, 2));
1729
1730            // At the minima, the Hessian should be positive definite
1731            // We'll check that the diagonal entries are positive
1732            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            // Compute the determinant to check positive definiteness
1739            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}