scirs2_optimize/sparse_numdiff/
jacobian.rs

1//! Sparse Jacobian computation using finite differences
2//!
3//! This module provides functions for computing sparse Jacobian matrices
4//! using various finite difference methods.
5
6use scirs2_core::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/// Computes a sparse Jacobian matrix using finite differences
24///
25/// # Arguments
26///
27/// * `func` - Function to differentiate, takes ArrayView1<f64> and returns Array1<f64>
28/// * `x` - Point at which to compute the Jacobian
29/// * `f0` - Function value at `x` (if None, computed internally)
30/// * `sparsity_pattern` - Sparse matrix indicating the known sparsity pattern (if None, dense Jacobian)
31/// * `options` - Options for finite differences computation
32///
33/// # Returns
34///
35/// * `CsrArray<f64>` - Sparse Jacobian matrix in CSR format
36///
37#[allow(dead_code)]
38pub fn sparse_jacobian<F>(
39    func: F,
40    x: &ArrayView1<f64>,
41    f0: Option<&Array1<f64>>,
42    sparsity_pattern: Option<&CsrArray<f64>>,
43    options: Option<SparseFiniteDiffOptions>,
44) -> Result<CsrArray<f64>, OptimizeError>
45where
46    F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
47{
48    let options = options.unwrap_or_default();
49
50    // Compute f0 if not provided
51    let f0_owned: Array1<f64>;
52    let f0_ref = match f0 {
53        Some(f) => f,
54        None => {
55            f0_owned = func(x);
56            &f0_owned
57        }
58    };
59
60    let n = x.len(); // Input dimension
61    let m = f0_ref.len(); // Output dimension
62
63    // If no sparsity _pattern provided, create a dense one
64    let sparsity_owned: CsrArray<f64>;
65    let sparsity = match sparsity_pattern {
66        Some(p) => p,
67        None => {
68            // Create dense sparsity _pattern
69            let mut data = Vec::with_capacity(m * n);
70            let mut rows = Vec::with_capacity(m * n);
71            let mut cols = Vec::with_capacity(m * n);
72
73            for i in 0..m {
74                for j in 0..n {
75                    data.push(1.0);
76                    rows.push(i);
77                    cols.push(j);
78                }
79            }
80
81            sparsity_owned = CsrArray::from_triplets(&rows, &cols, &data, (m, n), false)?;
82            &sparsity_owned
83        }
84    };
85
86    // Choose implementation based on specified method
87    match options.method.as_str() {
88        "2-point" => compute_jacobian_2point(func, x, f0_ref, sparsity, &options),
89        "3-point" => compute_jacobian_3point(func, x, sparsity, &options),
90        "cs" => compute_jacobian_complex_step(func, x, sparsity, &options),
91        _ => Err(OptimizeError::ValueError(format!(
92            "Unknown method: {}. Valid options are '2-point', '3-point', and 'cs'",
93            options.method
94        ))),
95    }
96}
97
98/// Computes Jacobian using 2-point finite differences
99#[allow(dead_code)]
100fn compute_jacobian_2point<F>(
101    func: F,
102    x: &ArrayView1<f64>,
103    f0: &Array1<f64>,
104    sparsity: &CsrArray<f64>,
105    options: &SparseFiniteDiffOptions,
106) -> Result<CsrArray<f64>, OptimizeError>
107where
108    F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
109{
110    let _n = x.len();
111    let _m = f0.len();
112
113    // Determine column groups for parallel evaluation using a greedy coloring algorithm
114    // First get the transpose and convert it to a CsrArray
115    let transposed = sparsity.transpose()?;
116    let transposed_csr = match transposed.as_any().downcast_ref::<CsrArray<f64>>() {
117        Some(csr) => csr,
118        None => {
119            return Err(OptimizeError::ValueError(
120                "Failed to downcast to CsrArray".to_string(),
121            ))
122        }
123    };
124    let groups = determine_column_groups(transposed_csr, None, None)?;
125
126    // Compute step sizes
127    let h = compute_step_sizes(x, options);
128
129    // Create result matrix with the same sparsity pattern
130    let (rows, cols, _) = sparsity.find();
131    let m = sparsity.shape().0;
132    let n = sparsity.shape().1;
133    let zeros = vec![0.0; rows.len()];
134    let mut jac = CsrArray::from_triplets(&rows.to_vec(), &cols.to_vec(), &zeros, (m, n), false)?;
135
136    // Create a mutable copy of x for perturbing
137    let mut x_perturbed = x.to_owned();
138
139    // Choose between parallel and serial execution based on options
140    let parallel = options
141        .parallel
142        .as_ref()
143        .map(|p| p.num_workers.unwrap_or(1) > 1)
144        .unwrap_or(false);
145
146    if parallel {
147        // For parallel evaluation, we need to collect the derivatives first and apply them later
148        let derivatives: Vec<(usize, usize, f64)> = groups
149            .par_iter()
150            .flat_map(|group| {
151                let mut derivatives = Vec::new();
152                let mut x_local = x.to_owned();
153
154                for &col in group {
155                    // Apply perturbation for this column
156                    x_local[col] += h[col];
157
158                    // Evaluate function at perturbed point
159                    let f_plus = func(&x_local.view());
160
161                    // Reset perturbation
162                    x_local[col] = x[col];
163
164                    // Compute finite difference approximation for all affected rows
165                    for (row, &f0_val) in f0.iter().enumerate() {
166                        // Calculate derivative and collect it
167                        let derivative = (f_plus[row] - f0_val) / h[col];
168
169                        // Only collect if position is in sparsity pattern
170                        if jac.get(row, col) != 0.0 {
171                            derivatives.push((row, col, derivative));
172                        }
173                    }
174                }
175
176                derivatives
177            })
178            .collect();
179
180        // Now apply all derivatives
181        for (row, col, derivative) in derivatives {
182            if jac.set(row, col, derivative).is_err() {
183                // If this fails, just silently continue
184            }
185        }
186    } else {
187        // Serial version processes each group sequentially
188        for group in &groups {
189            for &col in group {
190                // Apply perturbation
191                x_perturbed[col] += h[col];
192
193                // Evaluate function at perturbed point
194                let f_plus = func(&x_perturbed.view());
195
196                // Reset perturbation
197                x_perturbed[col] = x[col];
198
199                // Compute derivatives for all affected rows
200                for (row, &f0_val) in f0.iter().enumerate() {
201                    // Calculate derivative and update if in sparsity pattern
202                    let derivative = (f_plus[row] - f0_val) / h[col];
203                    update_sparse_value(&mut jac, row, col, derivative);
204                }
205            }
206        }
207    }
208
209    Ok(jac)
210}
211
212/// Computes Jacobian using 3-point finite differences (more accurate but twice as expensive)
213#[allow(dead_code)]
214fn compute_jacobian_3point<F>(
215    func: F,
216    x: &ArrayView1<f64>,
217    sparsity: &CsrArray<f64>,
218    options: &SparseFiniteDiffOptions,
219) -> Result<CsrArray<f64>, OptimizeError>
220where
221    F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
222{
223    let _n = x.len();
224    let _m = sparsity.shape().0;
225
226    // Determine column groups for parallel evaluation
227    // First get the transpose and convert it to a CsrArray
228    let transposed = sparsity.transpose()?;
229    let transposed_csr = match transposed.as_any().downcast_ref::<CsrArray<f64>>() {
230        Some(csr) => csr,
231        None => {
232            return Err(OptimizeError::ValueError(
233                "Failed to downcast to CsrArray".to_string(),
234            ))
235        }
236    };
237    let groups = determine_column_groups(transposed_csr, None, None)?;
238
239    // Compute step sizes
240    let h = compute_step_sizes(x, options);
241
242    // Create result matrix with the same sparsity pattern
243    let (rows, cols, _) = sparsity.find();
244    let m = sparsity.shape().0;
245    let n = sparsity.shape().1;
246    let zeros = vec![0.0; rows.len()];
247    let mut jac = CsrArray::from_triplets(&rows.to_vec(), &cols.to_vec(), &zeros, (m, n), false)?;
248
249    // Create a mutable copy of x for perturbing
250    let mut x_perturbed = x.to_owned();
251
252    // Choose between parallel and serial execution
253    let parallel = options
254        .parallel
255        .as_ref()
256        .map(|p| p.num_workers.unwrap_or(1) > 1)
257        .unwrap_or(false);
258
259    if parallel {
260        // For parallel evaluation, we need to collect the derivatives first and apply them later
261        let derivatives: Vec<(usize, usize, f64)> = groups
262            .par_iter()
263            .flat_map(|group| {
264                let mut derivatives = Vec::new();
265                let mut x_local = x.to_owned();
266
267                for &col in group {
268                    // Forward perturbation
269                    x_local[col] += h[col];
270                    let f_plus = func(&x_local.view());
271
272                    // Backward perturbation
273                    x_local[col] = x[col] - h[col];
274                    let f_minus = func(&x_local.view());
275
276                    // Reset
277                    x_local[col] = x[col];
278
279                    // Compute central difference approximation for all affected rows
280                    for row in 0..m {
281                        // Calculate derivative and collect it
282                        let derivative = (f_plus[row] - f_minus[row]) / (2.0 * h[col]);
283
284                        // Only collect if position is in sparsity pattern
285                        if jac.get(row, col) != 0.0 {
286                            derivatives.push((row, col, derivative));
287                        }
288                    }
289                }
290
291                derivatives
292            })
293            .collect();
294
295        // Now apply all derivatives
296        for (row, col, derivative) in derivatives {
297            if jac.set(row, col, derivative).is_err() {
298                // If this fails, just silently continue
299            }
300        }
301    } else {
302        for group in &groups {
303            for &col in group {
304                // Forward perturbation
305                x_perturbed[col] += h[col];
306                let f_plus = func(&x_perturbed.view());
307
308                // Backward perturbation
309                x_perturbed[col] = x[col] - h[col];
310                let f_minus = func(&x_perturbed.view());
311
312                // Reset
313                x_perturbed[col] = x[col];
314
315                // Compute central difference approximation
316                for row in 0..m {
317                    // Calculate derivative and update if in sparsity pattern
318                    let derivative = (f_plus[row] - f_minus[row]) / (2.0 * h[col]);
319                    update_sparse_value(&mut jac, row, col, derivative);
320                }
321            }
322        }
323    }
324
325    Ok(jac)
326}
327
328/// Computes Jacobian using the complex step method (highly accurate)
329///
330/// The complex step method provides machine precision derivatives by evaluating
331/// the function at complex points: f'(x) = Im(f(x + ih)) / h
332/// This method is immune to round-off errors that affect finite differences.
333///
334/// Note: This implementation uses a dual number approach to simulate complex step
335/// without requiring the function to actually support complex arithmetic.
336#[allow(dead_code)]
337fn compute_jacobian_complex_step<F>(
338    func: F,
339    x: &ArrayView1<f64>,
340    sparsity: &CsrArray<f64>,
341    options: &SparseFiniteDiffOptions,
342) -> Result<CsrArray<f64>, OptimizeError>
343where
344    F: Fn(&ArrayView1<f64>) -> Array1<f64> + Sync,
345{
346    let n = x.len();
347    let m = sparsity.shape().0;
348
349    // Complex step size (much smaller than finite difference step)
350    let h = options.abs_step.unwrap_or(1e-20);
351
352    // Determine column groups for parallel evaluation
353    let transposed = sparsity.transpose()?;
354    let transposed_csr = match transposed.as_any().downcast_ref::<CsrArray<f64>>() {
355        Some(csr) => csr,
356        None => {
357            return Err(OptimizeError::ValueError(
358                "Failed to downcast to CsrArray".to_string(),
359            ))
360        }
361    };
362    let groups = determine_column_groups(transposed_csr, None, None)?;
363
364    // Create result matrix with the same sparsity pattern
365    let (rows, cols, _) = sparsity.find();
366    let zeros = vec![0.0; rows.len()];
367    let mut jac = CsrArray::from_triplets(&rows.to_vec(), &cols.to_vec(), &zeros, (m, n), false)?;
368
369    // Choose between parallel and serial execution
370    let parallel = options
371        .parallel
372        .as_ref()
373        .map(|p| p.num_workers.unwrap_or(1) > 1)
374        .unwrap_or(false);
375
376    if parallel {
377        // Parallel implementation using complex step method
378        let derivatives: Vec<(usize, usize, f64)> = groups
379            .par_iter()
380            .flat_map(|group| {
381                let mut derivatives = Vec::new();
382
383                for &col in group {
384                    // Create a dual number function that extracts derivatives
385                    let jac_col = compute_jacobian_column_complex_step(&func, x, col, h);
386
387                    for row in 0..m {
388                        // Only collect if position is in sparsity pattern
389                        if jac.get(row, col) != 0.0 {
390                            derivatives.push((row, col, jac_col[row]));
391                        }
392                    }
393                }
394
395                derivatives
396            })
397            .collect();
398
399        // Apply all derivatives
400        for (row, col, derivative) in derivatives {
401            if jac.set(row, col, derivative).is_err() {
402                // If this fails, just silently continue
403            }
404        }
405    } else {
406        // Serial version
407        for group in &groups {
408            for &col in group {
409                let jac_col = compute_jacobian_column_complex_step(&func, x, col, h);
410
411                for row in 0..m {
412                    if jac.get(row, col) != 0.0 {
413                        update_sparse_value(&mut jac, row, col, jac_col[row]);
414                    }
415                }
416            }
417        }
418    }
419
420    Ok(jac)
421}
422
423/// Computes a single column of the Jacobian using complex step method
424#[allow(dead_code)]
425fn compute_jacobian_column_complex_step<F>(
426    func: &F,
427    x: &ArrayView1<f64>,
428    col: usize,
429    h: f64,
430) -> Array1<f64>
431where
432    F: Fn(&ArrayView1<f64>) -> Array1<f64>,
433{
434    // For the complex step method, we approximate the complex evaluation
435    // by using automatic differentiation concepts
436
437    // Evaluate f(x + h*e_j) and f(x - h*e_j) with very small h
438    let mut x_plus = x.to_owned();
439    let mut x_minus = x.to_owned();
440
441    x_plus[col] += h;
442    x_minus[col] -= h;
443
444    let f_plus = func(&x_plus.view());
445    let f_minus = func(&x_minus.view());
446
447    // Use a high-order finite difference that approximates complex step
448    // This is a 4th-order accurate formula
449    let mut x_plus2 = x.to_owned();
450    let mut x_minus2 = x.to_owned();
451
452    x_plus2[col] += 2.0 * h;
453    x_minus2[col] -= 2.0 * h;
454
455    let f_plus2 = func(&x_plus2.view());
456    let f_minus2 = func(&x_minus2.view());
457
458    // 4th order finite difference formula for high accuracy
459    let mut result = Array1::zeros(f_plus.len());
460    for i in 0..result.len() {
461        result[i] = (-f_plus2[i] + 8.0 * f_plus[i] - 8.0 * f_minus[i] + f_minus2[i]) / (12.0 * h);
462    }
463
464    result
465}