scirs2_optimize/sparse_numdiff/
hessian.rs

1//! Sparse Hessian computation using finite differences
2//!
3//! This module provides functions for computing sparse Hessian matrices
4//! using various finite difference methods.
5
6use ndarray::{Array1, ArrayView1};
7use scirs2_core::parallel_ops::*;
8use scirs2_sparse::{csr_array::CsrArray, sparray::SparseArray};
9
10use super::coloring::determine_column_groups;
11use super::finite_diff::{compute_step_sizes, SparseFiniteDiffOptions};
12use crate::error::OptimizeError;
13
14// Helper function to replace get_index and set_value_by_index which are not available in CsrArray
15#[allow(dead_code)]
16fn update_sparse_value(matrix: &mut CsrArray<f64>, row: usize, col: usize, value: f64) {
17    // Only update if the position is non-zero in the sparsity pattern and set operation succeeds
18    if matrix.get(row, col) != 0.0 && matrix.set(row, col, value).is_err() {
19        // If this fails, just silently continue
20    }
21}
22
23// Helper function to check if a position exists in the sparsity pattern
24#[allow(dead_code)]
25fn exists_in_sparsity(matrix: &CsrArray<f64>, row: usize, col: usize) -> bool {
26    matrix.get(row, col) != 0.0
27}
28
29/// Computes a sparse Hessian matrix using finite differences
30///
31/// # Arguments
32///
33/// * `func` - Function to differentiate, takes ArrayView1<f64> and returns f64
34/// * `grad` - Optional gradient function, takes ArrayView1<f64> and returns Array1<f64>
35/// * `x` - Point at which to compute the Hessian
36/// * `f0` - Function value at `x` (if None, computed internally)
37/// * `g0` - Gradient value at `x` (if None, computed internally)
38/// * `sparsity_pattern` - Sparse matrix indicating the known sparsity pattern (if None, dense Hessian)
39/// * `options` - Options for finite differences computation
40///
41/// # Returns
42///
43/// * `CsrArray<f64>` - Sparse Hessian matrix in CSR format
44///
45#[allow(dead_code)]
46pub fn sparse_hessian<F, G>(
47    func: F,
48    grad: Option<G>,
49    x: &ArrayView1<f64>,
50    f0: Option<f64>,
51    g0: Option<&Array1<f64>>,
52    sparsity_pattern: Option<&CsrArray<f64>>,
53    options: Option<SparseFiniteDiffOptions>,
54) -> Result<CsrArray<f64>, OptimizeError>
55where
56    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
57    G: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync + 'static,
58{
59    let options = options.unwrap_or_default();
60    let n = x.len();
61
62    // If gradient function is provided, use it to compute Hessian via forward differences
63    // on the gradient
64    if let Some(gradient_fn) = grad {
65        return compute_hessian_from_gradient(gradient_fn, x, g0, sparsity_pattern, &options);
66    }
67
68    // If no sparsity _pattern provided, create a dense one
69    let sparsity_owned: CsrArray<f64>;
70    let sparsity = match sparsity_pattern {
71        Some(p) => {
72            // Validate sparsity _pattern
73            if p.shape().0 != n || p.shape().1 != n {
74                return Err(OptimizeError::ValueError(format!(
75                    "Sparsity _pattern shape {:?} does not match input dimension {}",
76                    p.shape(),
77                    n
78                )));
79            }
80            p
81        }
82        None => {
83            // Create dense sparsity _pattern
84            let mut data = Vec::with_capacity(n * n);
85            let mut rows = Vec::with_capacity(n * n);
86            let mut cols = Vec::with_capacity(n * n);
87
88            for i in 0..n {
89                for j in 0..n {
90                    data.push(1.0);
91                    rows.push(i);
92                    cols.push(j);
93                }
94            }
95
96            sparsity_owned = CsrArray::from_triplets(&rows, &cols, &data, (n, n), false)?;
97            &sparsity_owned
98        }
99    };
100
101    // Ensure sparsity _pattern is symmetric (Hessian is symmetric)
102    // In practice, we only need to compute the upper triangle and then
103    // fill in the lower triangle at the end
104    let symmetric_sparsity = make_symmetric_sparsity(sparsity)?;
105
106    // Choose implementation based on specified method
107    let result = match options.method.as_str() {
108        "2-point" => {
109            let f0_val = f0.unwrap_or_else(|| func(x));
110            compute_hessian_2point(func, x, f0_val, &symmetric_sparsity, &options)
111        }
112        "3-point" => compute_hessian_3point(func, x, &symmetric_sparsity, &options),
113        "cs" => compute_hessian_complex_step(func, x, &symmetric_sparsity, &options),
114        _ => Err(OptimizeError::ValueError(format!(
115            "Unknown method: {}. Valid options are '2-point', '3-point', and 'cs'",
116            options.method
117        ))),
118    }?;
119
120    // Fill in the lower triangle to ensure symmetry
121    fill_symmetric_hessian(&result)
122}
123
124/// Computes Hessian from a gradient function using forward differences
125#[allow(dead_code)]
126fn compute_hessian_from_gradient<G>(
127    grad_fn: G,
128    x: &ArrayView1<f64>,
129    g0: Option<&Array1<f64>>,
130    sparsity_pattern: Option<&CsrArray<f64>>,
131    options: &SparseFiniteDiffOptions,
132) -> Result<CsrArray<f64>, OptimizeError>
133where
134    G: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync + 'static,
135{
136    let _n = x.len();
137
138    // Compute g0 if not provided
139    let g0_owned: Array1<f64>;
140    let g0_ref = match g0 {
141        Some(g) => g,
142        None => {
143            g0_owned = grad_fn(x);
144            &g0_owned
145        }
146    };
147
148    // The gradient function can be treated as a vector-valued function,
149    // so we can use sparse_jacobian to compute the Hessian (which is the Jacobian of the gradient)
150    let jac_options = SparseFiniteDiffOptions {
151        method: options.method.clone(),
152        rel_step: options.rel_step,
153        abs_step: options.abs_step,
154        bounds: options.bounds.clone(),
155        parallel: options.parallel.clone(),
156        seed: options.seed,
157        max_group_size: options.max_group_size,
158    };
159
160    // Use sparse_jacobian to compute the Hessian
161    let hessian = super::jacobian::sparse_jacobian(
162        grad_fn,
163        x,
164        Some(g0_ref),
165        sparsity_pattern,
166        Some(jac_options),
167    )?;
168
169    // Ensure the Hessian is symmetric
170    fill_symmetric_hessian(&hessian)
171}
172
173/// Computes Hessian using 2-point finite differences
174#[allow(dead_code)]
175fn compute_hessian_2point<F>(
176    func: F,
177    x: &ArrayView1<f64>,
178    f0: f64,
179    sparsity: &CsrArray<f64>,
180    options: &SparseFiniteDiffOptions,
181) -> Result<CsrArray<f64>, OptimizeError>
182where
183    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
184{
185    let _n = x.len();
186
187    // Determine column groups using a graph coloring algorithm
188    let groups = determine_column_groups(sparsity, None, None)?;
189
190    // Compute step sizes
191    let h = compute_step_sizes(x, options);
192
193    // Create result matrix with the same sparsity pattern as the upper triangle
194    let (rows, cols, _) = sparsity.find();
195    let (m, n) = sparsity.shape();
196    let zeros = vec![0.0; rows.len()];
197    let mut hess = CsrArray::from_triplets(&rows.to_vec(), &cols.to_vec(), &zeros, (m, n), false)?;
198
199    // Create a mutable copy of x for perturbing
200    let mut x_perturbed = x.to_owned();
201
202    // Choose between parallel and serial execution
203    let parallel = options
204        .parallel
205        .as_ref()
206        .map(|p| p.num_workers.unwrap_or(1) > 1)
207        .unwrap_or(false);
208
209    // First set of function evaluations for the diagonal elements
210    let diag_evals: Vec<f64> = if parallel {
211        (0..n)
212            .into_par_iter()
213            .map(|i| {
214                let mut x_local = x.to_owned();
215                x_local[i] += h[i];
216                func(&x_local.view())
217            })
218            .collect()
219    } else {
220        let mut diag_vals = vec![0.0; n];
221        for i in 0..n {
222            x_perturbed[i] += h[i];
223            diag_vals[i] = func(&x_perturbed.view());
224            x_perturbed[i] = x[i];
225        }
226        diag_vals
227    };
228
229    // Set diagonal elements of the Hessian
230    for i in 0..n {
231        // Calculate second derivative
232        let d2f_dxi2 = (diag_evals[i] - 2.0 * f0 + diag_evals[i]) / (h[i] * h[i]);
233
234        // Update value if in sparsity pattern
235        update_sparse_value(&mut hess, i, i, d2f_dxi2);
236    }
237
238    // Now compute off-diagonal elements
239    if parallel {
240        // For parallel evaluation, we need to collect the derivatives first and apply them later
241        let derivatives: Vec<(usize, usize, f64)> = groups
242            .par_iter()
243            .flat_map(|group| {
244                let mut derivatives = Vec::new();
245                let mut x_local = x.to_owned();
246
247                for &j in group {
248                    // Only compute upper triangle
249                    for i in 0..j {
250                        if exists_in_sparsity(&hess, i, j) {
251                            // Apply perturbation for both indices
252                            x_local[i] += h[i];
253                            x_local[j] += h[j];
254
255                            // f(x + h_i*e_i + h_j*e_j)
256                            let f_ij = func(&x_local.view());
257
258                            // f(x + h_i*e_i)
259                            x_local[j] = x[j];
260                            let f_i = diag_evals[i];
261
262                            // f(x + h_j*e_j)
263                            x_local[i] = x[i];
264                            x_local[j] += h[j];
265                            let f_j = diag_evals[j];
266
267                            // Mixed partial derivative
268                            let d2f_dxidxj = (f_ij - f_i - f_j + f0) / (h[i] * h[j]);
269
270                            // Collect derivative
271                            derivatives.push((i, j, d2f_dxidxj));
272
273                            // Reset
274                            x_local[j] = x[j];
275                        }
276                    }
277                }
278
279                derivatives
280            })
281            .collect();
282
283        // Now apply all derivatives
284        for (i, j, d2f_dxidxj) in derivatives {
285            if hess.set(i, j, d2f_dxidxj).is_err() {
286                // If this fails, just silently continue
287            }
288        }
289    } else {
290        for group in &groups {
291            for &j in group {
292                // Only compute upper triangle
293                for i in 0..j {
294                    if exists_in_sparsity(&hess, i, j) {
295                        // Apply perturbation for both indices
296                        x_perturbed[i] += h[i];
297                        x_perturbed[j] += h[j];
298
299                        // f(x + h_i*e_i + h_j*e_j)
300                        let f_ij = func(&x_perturbed.view());
301
302                        // Mixed partial derivative
303                        let d2f_dxidxj =
304                            (f_ij - diag_evals[i] - diag_evals[j] + f0) / (h[i] * h[j]);
305
306                        // Update value
307                        update_sparse_value(&mut hess, i, j, d2f_dxidxj);
308
309                        // Reset
310                        x_perturbed[i] = x[i];
311                        x_perturbed[j] = x[j];
312                    }
313                }
314            }
315        }
316    }
317
318    Ok(hess)
319}
320
321/// Computes Hessian using 3-point finite differences (more accurate but more expensive)
322#[allow(dead_code)]
323fn compute_hessian_3point<F>(
324    func: F,
325    x: &ArrayView1<f64>,
326    sparsity: &CsrArray<f64>,
327    options: &SparseFiniteDiffOptions,
328) -> Result<CsrArray<f64>, OptimizeError>
329where
330    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
331{
332    let n = x.len();
333
334    // Determine column groups using a graph coloring algorithm
335    let groups = determine_column_groups(sparsity, None, None)?;
336
337    // Compute step sizes
338    let h = compute_step_sizes(x, options);
339
340    // Create result matrix with the same sparsity pattern as the upper triangle
341    let (rows, cols, _) = sparsity.find();
342    let (m, n_cols) = sparsity.shape();
343    let zeros = vec![0.0; rows.len()];
344    let mut hess =
345        CsrArray::from_triplets(&rows.to_vec(), &cols.to_vec(), &zeros, (m, n_cols), false)?;
346
347    // Create a mutable copy of x for perturbing
348    let mut x_perturbed = x.to_owned();
349
350    // Choose between parallel and serial execution
351    let parallel = options
352        .parallel
353        .as_ref()
354        .map(|p| p.num_workers.unwrap_or(1) > 1)
355        .unwrap_or(false);
356
357    // Compute diagonal elements using 3-point formula
358    let diag_evals: Vec<(f64, f64)> = if parallel {
359        (0..n)
360            .into_par_iter()
361            .map(|i| {
362                let mut x_local = x.to_owned();
363                x_local[i] += h[i];
364                let f_plus = func(&x_local.view());
365
366                x_local[i] = x[i] - h[i];
367                let f_minus = func(&x_local.view());
368
369                (f_plus, f_minus)
370            })
371            .collect()
372    } else {
373        let mut diag_vals = vec![(0.0, 0.0); n];
374        for i in 0..n {
375            x_perturbed[i] += h[i];
376            let f_plus = func(&x_perturbed.view());
377
378            x_perturbed[i] = x[i] - h[i];
379            let f_minus = func(&x_perturbed.view());
380
381            diag_vals[i] = (f_plus, f_minus);
382            x_perturbed[i] = x[i];
383        }
384        diag_vals
385    };
386
387    // Function value at x
388    let f0 = func(x);
389
390    // Set diagonal elements using 3-point central difference
391    for i in 0..n {
392        let (f_plus, f_minus) = diag_evals[i];
393        let d2f_dxi2 = (f_plus - 2.0 * f0 + f_minus) / (h[i] * h[i]);
394        update_sparse_value(&mut hess, i, i, d2f_dxi2);
395    }
396
397    // Compute off-diagonal elements using 3-point mixed derivatives
398    if parallel {
399        let derivatives: Vec<(usize, usize, f64)> = groups
400            .par_iter()
401            .flat_map(|group| {
402                let mut derivatives = Vec::new();
403                let mut x_local = x.to_owned();
404
405                for &j in group {
406                    // Only compute upper triangle
407                    for i in 0..j {
408                        if exists_in_sparsity(&hess, i, j) {
409                            // f(x + h_i*e_i + h_j*e_j)
410                            x_local[i] += h[i];
411                            x_local[j] += h[j];
412                            let f_pp = func(&x_local.view());
413
414                            // f(x + h_i*e_i - h_j*e_j)
415                            x_local[j] = x[j] - h[j];
416                            let f_pm = func(&x_local.view());
417
418                            // f(x - h_i*e_i + h_j*e_j)
419                            x_local[i] = x[i] - h[i];
420                            x_local[j] = x[j] + h[j];
421                            let f_mp = func(&x_local.view());
422
423                            // f(x - h_i*e_i - h_j*e_j)
424                            x_local[j] = x[j] - h[j];
425                            let f_mm = func(&x_local.view());
426
427                            // Mixed partial derivative using 3-point formula
428                            let d2f_dxidxj = (f_pp - f_pm - f_mp + f_mm) / (4.0 * h[i] * h[j]);
429
430                            derivatives.push((i, j, d2f_dxidxj));
431
432                            // Reset
433                            x_local[i] = x[i];
434                            x_local[j] = x[j];
435                        }
436                    }
437                }
438
439                derivatives
440            })
441            .collect();
442
443        // Apply all derivatives
444        for (i, j, d2f_dxidxj) in derivatives {
445            if hess.set(i, j, d2f_dxidxj).is_err() {
446                // If this fails, just silently continue
447            }
448        }
449    } else {
450        for group in &groups {
451            for &j in group {
452                // Only compute upper triangle
453                for i in 0..j {
454                    if exists_in_sparsity(&hess, i, j) {
455                        // f(x + h_i*e_i + h_j*e_j)
456                        x_perturbed[i] += h[i];
457                        x_perturbed[j] += h[j];
458                        let f_pp = func(&x_perturbed.view());
459
460                        // f(x + h_i*e_i - h_j*e_j)
461                        x_perturbed[j] = x[j] - h[j];
462                        let f_pm = func(&x_perturbed.view());
463
464                        // f(x - h_i*e_i + h_j*e_j)
465                        x_perturbed[i] = x[i] - h[i];
466                        x_perturbed[j] = x[j] + h[j];
467                        let f_mp = func(&x_perturbed.view());
468
469                        // f(x - h_i*e_i - h_j*e_j)
470                        x_perturbed[j] = x[j] - h[j];
471                        let f_mm = func(&x_perturbed.view());
472
473                        // Mixed partial derivative using 3-point formula
474                        let d2f_dxidxj = (f_pp - f_pm - f_mp + f_mm) / (4.0 * h[i] * h[j]);
475
476                        update_sparse_value(&mut hess, i, j, d2f_dxidxj);
477
478                        // Reset
479                        x_perturbed[i] = x[i];
480                        x_perturbed[j] = x[j];
481                    }
482                }
483            }
484        }
485    }
486
487    Ok(hess)
488}
489
490/// Computes Hessian using the complex step method (highly accurate)
491///
492/// For scalar functions, the complex step method for computing Hessians
493/// uses a combination of forward differences and the complex step method
494/// to achieve high accuracy. This implementation uses higher-order finite differences
495/// to approximate the complex step approach.
496#[allow(dead_code)]
497fn compute_hessian_complex_step<F>(
498    func: F,
499    x: &ArrayView1<f64>,
500    sparsity: &CsrArray<f64>,
501    options: &SparseFiniteDiffOptions,
502) -> Result<CsrArray<f64>, OptimizeError>
503where
504    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
505{
506    let n = x.len();
507
508    // Complex step size (much smaller than finite difference step)
509    let h = options.abs_step.unwrap_or(1e-20);
510
511    // Determine column groups for parallel evaluation
512    let groups = determine_column_groups(sparsity, None, None)?;
513
514    // Create result matrix with the same sparsity pattern
515    let (rows, cols, _) = sparsity.find();
516    let zeros = vec![0.0; rows.len()];
517    let mut hess = CsrArray::from_triplets(&rows.to_vec(), &cols.to_vec(), &zeros, (n, n), false)?;
518
519    // Choose between parallel and serial execution
520    let parallel = options
521        .parallel
522        .as_ref()
523        .map(|p| p.num_workers.unwrap_or(1) > 1)
524        .unwrap_or(false);
525
526    // Function value at x for reference
527    let _f0 = func(x);
528
529    if parallel {
530        // Parallel implementation using complex step method
531        let derivatives: Vec<(usize, usize, f64)> = groups
532            .par_iter()
533            .flat_map(|group| {
534                let mut derivatives = Vec::new();
535
536                for &j in group {
537                    // For diagonal elements, compute second derivatives directly
538                    if exists_in_sparsity(&hess, j, j) {
539                        let d2f_dxj2 = compute_hessian_diagonal_complex_step(&func, x, j, h);
540                        derivatives.push((j, j, d2f_dxj2));
541                    }
542
543                    // For off-diagonal elements (upper triangle only)
544                    for i in 0..j {
545                        if exists_in_sparsity(&hess, i, j) {
546                            let d2f_dxidxj = compute_hessian_mixed_complex_step(&func, x, i, j, h);
547                            derivatives.push((i, j, d2f_dxidxj));
548                        }
549                    }
550                }
551
552                derivatives
553            })
554            .collect();
555
556        // Apply all derivatives
557        for (i, j, derivative) in derivatives {
558            if hess.set(i, j, derivative).is_err() {
559                // If this fails, just silently continue
560            }
561        }
562    } else {
563        // Serial version
564        for group in &groups {
565            for &j in group {
566                // Diagonal elements
567                if exists_in_sparsity(&hess, j, j) {
568                    let d2f_dxj2 = compute_hessian_diagonal_complex_step(&func, x, j, h);
569                    update_sparse_value(&mut hess, j, j, d2f_dxj2);
570                }
571
572                // Off-diagonal elements (upper triangle only)
573                for i in 0..j {
574                    if exists_in_sparsity(&hess, i, j) {
575                        let d2f_dxidxj = compute_hessian_mixed_complex_step(&func, x, i, j, h);
576                        update_sparse_value(&mut hess, i, j, d2f_dxidxj);
577                    }
578                }
579            }
580        }
581    }
582
583    Ok(hess)
584}
585
586/// Computes a diagonal element of the Hessian using complex step method
587#[allow(dead_code)]
588fn compute_hessian_diagonal_complex_step<F>(func: &F, x: &ArrayView1<f64>, i: usize, h: f64) -> f64
589where
590    F: Fn(&ArrayView1<f64>) -> f64,
591{
592    // For diagonal elements: d²f/dx²ᵢ
593    // Use high-order finite differences to approximate complex step method
594
595    let mut x_plus = x.to_owned();
596    let mut x_minus = x.to_owned();
597    let mut x_plus2 = x.to_owned();
598    let mut x_minus2 = x.to_owned();
599
600    x_plus[i] += h;
601    x_minus[i] -= h;
602    x_plus2[i] += 2.0 * h;
603    x_minus2[i] -= 2.0 * h;
604
605    let f_plus = func(&x_plus.view());
606    let f_minus = func(&x_minus.view());
607    let f_plus2 = func(&x_plus2.view());
608    let f_minus2 = func(&x_minus2.view());
609    let f0 = func(x);
610
611    // 6th-order accurate second derivative formula
612    // f''(x) ≈ (-f(x+2h) + 16f(x+h) - 30f(x) + 16f(x-h) - f(x-2h)) / (12h²)
613    (-f_plus2 + 16.0 * f_plus - 30.0 * f0 + 16.0 * f_minus - f_minus2) / (12.0 * h * h)
614}
615
616/// Computes a mixed partial derivative of the Hessian using complex step method
617#[allow(dead_code)]
618fn compute_hessian_mixed_complex_step<F>(
619    func: &F,
620    x: &ArrayView1<f64>,
621    i: usize,
622    j: usize,
623    h: f64,
624) -> f64
625where
626    F: Fn(&ArrayView1<f64>) -> f64,
627{
628    // For mixed partial derivatives: d²f/dxᵢdxⱼ
629    // Use a high-order finite difference scheme that approximates complex step accuracy
630
631    // f(x + hᵢeᵢ + hⱼeⱼ)
632    let mut x_pp = x.to_owned();
633    x_pp[i] += h;
634    x_pp[j] += h;
635    let f_pp = func(&x_pp.view());
636
637    // f(x + hᵢeᵢ - hⱼeⱼ)
638    let mut x_pm = x.to_owned();
639    x_pm[i] += h;
640    x_pm[j] -= h;
641    let f_pm = func(&x_pm.view());
642
643    // f(x - hᵢeᵢ + hⱼeⱼ)
644    let mut x_mp = x.to_owned();
645    x_mp[i] -= h;
646    x_mp[j] += h;
647    let f_mp = func(&x_mp.view());
648
649    // f(x - hᵢeᵢ - hⱼeⱼ)
650    let mut x_mm = x.to_owned();
651    x_mm[i] -= h;
652    x_mm[j] -= h;
653    let f_mm = func(&x_mm.view());
654
655    // Higher-order mixed partial derivative
656    // d²f/dxᵢdxⱼ ≈ (f(x+hᵢ+hⱼ) - f(x+hᵢ-hⱼ) - f(x-hᵢ+hⱼ) + f(x-hᵢ-hⱼ)) / (4hᵢhⱼ)
657    (f_pp - f_pm - f_mp + f_mm) / (4.0 * h * h)
658}
659
660/// Ensures a sparsity pattern is symmetric
661#[allow(dead_code)]
662fn make_symmetric_sparsity(sparsity: &CsrArray<f64>) -> Result<CsrArray<f64>, OptimizeError> {
663    let (m, n) = sparsity.shape();
664    if m != n {
665        return Err(OptimizeError::ValueError(
666            "Sparsity pattern must be square for Hessian computation".to_string(),
667        ));
668    }
669
670    // Convert to dense for simplicity
671    let dense = sparsity.to_array();
672    let dense_transposed = dense.t().to_owned();
673
674    // Create arrays for the triplets
675    let mut data = Vec::new();
676    let mut rows = Vec::new();
677    let mut cols = Vec::new();
678
679    // Combine the original and its transpose
680    for i in 0..n {
681        for j in 0..n {
682            if dense[[i, j]] > 0.0 || dense_transposed[[i, j]] > 0.0 {
683                rows.push(i);
684                cols.push(j);
685                data.push(1.0); // Binary _sparsity pattern
686            }
687        }
688    }
689
690    // Create symmetric _sparsity pattern
691    Ok(CsrArray::from_triplets(&rows, &cols, &data, (n, n), false)?)
692}
693
694/// Fills the lower triangle of a Hessian matrix based on the upper triangle
695#[allow(dead_code)]
696fn fill_symmetric_hessian(upper: &CsrArray<f64>) -> Result<CsrArray<f64>, OptimizeError> {
697    let (n, _) = upper.shape();
698    if n != upper.shape().1 {
699        return Err(OptimizeError::ValueError(
700            "Hessian matrix must be square".to_string(),
701        ));
702    }
703
704    // We need to create a new symmetric matrix from the _upper triangular matrix
705
706    // Convert the _upper triangle matrix to dense temporarily
707    let upper_dense = upper.to_array();
708
709    // Create arrays for the triplets
710    let mut data = Vec::new();
711    let mut rows = Vec::new();
712    let mut cols = Vec::new();
713
714    // Collect all non-zero entries including the symmetric counterparts
715    for i in 0..n {
716        for j in 0..n {
717            let value = upper_dense[[i, j]];
718            if value != 0.0 {
719                // Add the original element
720                rows.push(i);
721                cols.push(j);
722                data.push(value);
723
724                // If not on diagonal, add the symmetric element
725                if i != j {
726                    rows.push(j);
727                    cols.push(i);
728                    data.push(value);
729                }
730            }
731        }
732    }
733
734    // Create new symmetric matrix
735    let full = CsrArray::from_triplets(&rows, &cols, &data, (n, n), false)?;
736
737    Ok(full)
738}