scirs2_optimize/sparse_numdiff/
compression.rs

1//! Compression techniques for sparse matrices in numerical differentiation
2//!
3//! This module provides implementations of compression algorithms for
4//! sparse matrices to reduce the number of function evaluations required
5//! for finite differences.
6
7use crate::error::OptimizeError;
8use ndarray::{Array2, ArrayView2};
9// Unused import: Array1
10use scirs2_sparse::csr_array::CsrArray;
11use scirs2_sparse::SparseArray;
12use std::collections::HashSet;
13// Unused import: HashMap
14
15/// Type alias for the return type of compress_jacobian_pattern
16pub type CompressedJacobianPattern = (CsrArray<f64>, Array2<f64>, Array2<f64>);
17
18/// Compresses a sparse Jacobian pattern for more efficient finite differencing
19///
20/// Uses a simplified Curtis-Powell-Reid algorithm with graph coloring to group
21/// columns that can be computed simultaneously without interference.
22///
23/// # Arguments
24///
25/// * `sparsity` - Sparse matrix representing the Jacobian sparsity pattern
26///
27/// # Returns
28///
29/// * Compressed sparsity pattern and compression matrices (original, B, C)
30///   where B is the column compression matrix and C is the row compression matrix
31#[allow(dead_code)]
32pub fn compress_jacobian_pattern(
33    sparsity: &CsrArray<f64>,
34) -> Result<CompressedJacobianPattern, OptimizeError> {
35    let (m, n) = sparsity.shape();
36
37    // Perform column coloring: group columns that don't interfere
38    let coloring = color_jacobian_columns(sparsity)?;
39    let num_colors = coloring.iter().max().unwrap_or(&0) + 1;
40
41    // Build column compression matrix B (n x num_colors)
42    let mut b = Array2::zeros((n, num_colors));
43    for (col, &color) in coloring.iter().enumerate() {
44        b[[col, color]] = 1.0;
45    }
46
47    // Row compression matrix C is identity for Jacobian (m x m)
48    let c = Array2::eye(m);
49
50    Ok((sparsity.clone(), b, c))
51}
52
53/// Colors the columns of a Jacobian sparsity pattern using a greedy algorithm
54///
55/// Two columns can have the same color if they don't both have nonzeros in the same row
56#[allow(dead_code)]
57fn color_jacobian_columns(sparsity: &CsrArray<f64>) -> Result<Vec<usize>, OptimizeError> {
58    let (_m, n) = sparsity.shape();
59    let mut coloring = vec![0; n];
60
61    // For each column, find the lowest color that doesn't conflict
62    for col in 0..n {
63        let mut used_colors = HashSet::new();
64
65        // Find all rows where this column has a nonzero
66        let col_rows = get_column_nonzero_rows(sparsity, col);
67
68        // Check previously colored columns that share rows with this column
69        for prev_col in 0..col {
70            let prev_col_rows = get_column_nonzero_rows(sparsity, prev_col);
71
72            // If columns share any row, they can't have the same color
73            if col_rows.iter().any(|&row| prev_col_rows.contains(&row)) {
74                used_colors.insert(coloring[prev_col]);
75            }
76        }
77
78        // Assign the lowest available color
79        let mut color = 0;
80        while used_colors.contains(&color) {
81            color += 1;
82        }
83        coloring[col] = color;
84    }
85
86    Ok(coloring)
87}
88
89/// Helper function to get rows where a column has nonzero entries
90#[allow(dead_code)]
91fn get_column_nonzero_rows(sparsity: &CsrArray<f64>, col: usize) -> HashSet<usize> {
92    let mut rows = HashSet::new();
93    let (m, _) = sparsity.shape();
94
95    for row in 0..m {
96        let val = sparsity.get(row, col);
97        if val.abs() > 1e-15 {
98            rows.insert(row);
99        }
100    }
101
102    rows
103}
104
105/// Compresses a sparse Hessian pattern for more efficient finite differencing
106///
107/// Uses a distance-2 coloring algorithm suitable for symmetric Hessian matrices.
108/// Two columns can have the same color if they are at distance > 2 in the sparsity graph.
109///
110/// # Arguments
111///
112/// * `sparsity` - Sparse matrix representing the Hessian sparsity pattern
113///
114/// # Returns
115///
116/// * Compressed sparsity pattern and compression matrix P (n x num_colors)
117#[allow(dead_code)]
118pub fn compress_hessian_pattern(
119    sparsity: &CsrArray<f64>,
120) -> Result<(CsrArray<f64>, Array2<f64>), OptimizeError> {
121    let (n, _) = sparsity.shape();
122
123    // Perform distance-2 coloring for Hessian
124    let coloring = color_hessian_columns(sparsity)?;
125    let num_colors = coloring.iter().max().unwrap_or(&0) + 1;
126
127    // Build compression matrix P (n x num_colors)
128    let mut p = Array2::zeros((n, num_colors));
129    for (col, &color) in coloring.iter().enumerate() {
130        p[[col, color]] = 1.0;
131    }
132
133    Ok((sparsity.clone(), p))
134}
135
136/// Colors the columns of a Hessian sparsity pattern using distance-2 coloring
137///
138/// For symmetric matrices, two columns i and j can have the same color if:
139/// 1. They are not adjacent (H[i,j] = 0)
140/// 2. They don't share any common neighbors
141#[allow(dead_code)]
142fn color_hessian_columns(sparsity: &CsrArray<f64>) -> Result<Vec<usize>, OptimizeError> {
143    let (n, _) = sparsity.shape();
144    let mut coloring = vec![0; n];
145
146    // Build adjacency information for efficient neighbor lookup
147    let adjacency = build_adjacency_list(sparsity);
148
149    // For each column, find the lowest color that doesn't conflict
150    for col in 0..n {
151        let mut used_colors = HashSet::new();
152
153        // Get neighbors of current column
154        let neighbors = &adjacency[col];
155
156        // Check all previously colored columns
157        for prev_col in 0..col {
158            let prev_neighbors = &adjacency[prev_col];
159
160            // Check if columns are distance <= 2 apart
161            let distance_conflict =
162                // Distance 1: directly connected
163                neighbors.contains(&prev_col) ||
164                // Distance 2: share a common neighbor
165                neighbors.iter().any(|&n| prev_neighbors.contains(&n));
166
167            if distance_conflict {
168                used_colors.insert(coloring[prev_col]);
169            }
170        }
171
172        // Assign the lowest available color
173        let mut color = 0;
174        while used_colors.contains(&color) {
175            color += 1;
176        }
177        coloring[col] = color;
178    }
179
180    Ok(coloring)
181}
182
183/// Build adjacency list representation of the sparsity pattern
184#[allow(dead_code)]
185fn build_adjacency_list(sparsity: &CsrArray<f64>) -> Vec<HashSet<usize>> {
186    let (n, _) = sparsity.shape();
187    let mut adjacency = vec![HashSet::new(); n];
188
189    for i in 0..n {
190        for j in 0..n {
191            if i != j {
192                let val = sparsity.get(i, j);
193                if val.abs() > 1e-15 {
194                    adjacency[i].insert(j);
195                    adjacency[j].insert(i); // Symmetric
196                }
197            }
198        }
199    }
200
201    adjacency
202}
203
204/// Reconstructs a sparse Jacobian from compressed gradient evaluations
205///
206/// Takes the compressed gradient evaluations and reconstructs the full sparse Jacobian
207/// using the compression matrices. Each compressed gradient contains information about
208/// multiple columns that were grouped together during compression.
209///
210/// # Arguments
211///
212/// * `gradients` - Matrix of compressed gradient evaluations (m x num_colors)
213/// * `b` - Column compression matrix (n x num_colors)
214/// * `c` - Row compression matrix (m x m), typically identity for Jacobian
215/// * `sparsity` - Original sparsity pattern for reconstruction
216///
217/// # Returns
218///
219/// * Reconstructed sparse Jacobian
220#[allow(dead_code)]
221pub fn reconstruct_jacobian(
222    gradients: &ArrayView2<f64>,
223    b: &ArrayView2<f64>,
224    _c: &ArrayView2<f64>,
225    sparsity: &CsrArray<f64>,
226) -> Result<CsrArray<f64>, OptimizeError> {
227    let (m, n) = sparsity.shape();
228    let (grad_m, num_colors) = gradients.dim();
229    let (b_n, b_colors) = b.dim();
230
231    // Validate dimensions
232    if grad_m != m || b_n != n || b_colors != num_colors {
233        return Err(OptimizeError::InvalidInput(
234            "Dimension mismatch in reconstruction matrices".to_string(),
235        ));
236    }
237
238    // Create dense matrix for reconstruction, then convert to sparse
239    let mut jacobian_dense = Array2::zeros((m, n));
240
241    // For each color group, extract the columns
242    for color in 0..num_colors {
243        // Find all columns that belong to this color
244        let mut columns_in_color = Vec::new();
245        for col in 0..n {
246            if b[[col, color]].abs() > 1e-15 {
247                columns_in_color.push(col);
248            }
249        }
250
251        // The gradient for this color contains the sum of derivatives for all columns in the group
252        // Since we used proper coloring, we can extract individual columns
253        for &col in &columns_in_color {
254            for row in 0..m {
255                // Check if this position should be nonzero according to sparsity pattern
256                let val = sparsity.get(row, col);
257                if val.abs() > 1e-15 {
258                    // The gradient[row, color] contains the derivative ∂f_row/∂x_col
259                    jacobian_dense[[row, col]] = gradients[[row, color]];
260                }
261            }
262        }
263    }
264
265    // Convert dense matrix to sparse, preserving only the sparsity pattern
266    let mut row_indices = Vec::new();
267    let mut col_indices = Vec::new();
268    let mut values = Vec::new();
269
270    for row in 0..m {
271        for col in 0..n {
272            let sparsity_val = sparsity.get(row, col);
273            if sparsity_val.abs() > 1e-15 && jacobian_dense[[row, col]].abs() > 1e-15 {
274                row_indices.push(row);
275                col_indices.push(col);
276                values.push(jacobian_dense[[row, col]]);
277            }
278        }
279    }
280
281    // Create sparse matrix from the reconstructed values
282    CsrArray::from_triplets(&row_indices, &col_indices, &values, (m, n), false)
283        .map_err(|_| OptimizeError::InvalidInput("Failed to create sparse matrix".to_string()))
284}
285
286/// Reconstructs a sparse Hessian from compressed gradient evaluations using central differences
287///
288/// Takes compressed gradient evaluations and reconstructs the symmetric sparse Hessian.
289/// The reconstruction assumes that gradients were computed using central differences
290/// with the compressed perturbation vectors.
291///
292/// # Arguments
293///
294/// * `gradients_forward` - Forward difference gradients (n x num_colors)
295/// * `gradients_backward` - Backward difference gradients (n x num_colors)  
296/// * `p` - Compression matrix (n x num_colors)
297/// * `sparsity` - Original Hessian sparsity pattern
298/// * `h` - Step size used in finite differences
299///
300/// # Returns
301///
302/// * Reconstructed sparse Hessian
303#[allow(dead_code)]
304pub fn reconstruct_hessian_central_diff(
305    gradients_forward: &ArrayView2<f64>,
306    gradients_backward: &ArrayView2<f64>,
307    p: &ArrayView2<f64>,
308    sparsity: &CsrArray<f64>,
309    h: f64,
310) -> Result<CsrArray<f64>, OptimizeError> {
311    let (n, _) = sparsity.shape();
312    let (grad_n, num_colors) = gradients_forward.dim();
313    let (p_n, p_colors) = p.dim();
314
315    // Validate dimensions
316    if grad_n != n || p_n != n || p_colors != num_colors {
317        return Err(OptimizeError::InvalidInput(
318            "Dimension mismatch in Hessian reconstruction matrices".to_string(),
319        ));
320    }
321
322    if gradients_backward.dim() != (n, num_colors) {
323        return Err(OptimizeError::InvalidInput(
324            "Gradient matrices must have the same dimensions".to_string(),
325        ));
326    }
327
328    // Create dense matrix for reconstruction
329    let mut hessian_dense = Array2::zeros((n, n));
330
331    // Reconstruct using central differences: H = (grad_forward - grad_backward) / (2*h)
332    for color in 0..num_colors {
333        // Find columns that belong to this color
334        let mut columns_in_color = Vec::new();
335        for col in 0..n {
336            if p[[col, color]].abs() > 1e-15 {
337                columns_in_color.push(col);
338            }
339        }
340
341        // For each pair of variables in the same color group
342        for &col_i in &columns_in_color {
343            for row in 0..n {
344                // Check if (row, col_i) should be nonzero according to sparsity
345                if sparsity.get(row, col_i).abs() > 1e-15 {
346                    // Central difference approximation
347                    let h_val = (gradients_forward[[row, color]]
348                        - gradients_backward[[row, color]])
349                        / (2.0 * h);
350                    hessian_dense[[row, col_i]] = h_val;
351
352                    // Ensure symmetry for Hessian
353                    if row != col_i && sparsity.get(col_i, row).abs() > 1e-15 {
354                        hessian_dense[[col_i, row]] = h_val;
355                    }
356                }
357            }
358        }
359    }
360
361    // Convert to sparse matrix, preserving sparsity pattern
362    let mut row_indices = Vec::new();
363    let mut col_indices = Vec::new();
364    let mut values = Vec::new();
365
366    for row in 0..n {
367        for col in 0..n {
368            if sparsity.get(row, col).abs() > 1e-15 && hessian_dense[[row, col]].abs() > 1e-15 {
369                row_indices.push(row);
370                col_indices.push(col);
371                values.push(hessian_dense[[row, col]]);
372            }
373        }
374    }
375
376    // Create sparse Hessian
377    CsrArray::from_triplets(&row_indices, &col_indices, &values, (n, n), false)
378        .map_err(|_| OptimizeError::ValueError("Failed to create sparse Hessian".to_string()))
379}
380
381/// Simplified version of Hessian reconstruction for single gradient input
382#[allow(dead_code)]
383pub fn reconstruct_hessian(
384    gradients: &ArrayView2<f64>,
385    p: &ArrayView2<f64>,
386    sparsity: &CsrArray<f64>,
387) -> Result<CsrArray<f64>, OptimizeError> {
388    // For backward compatibility, assume we have only forward differences
389    // In practice, this would be less accurate than using central differences
390    let h = 1e-8; // Default step size
391    let gradients_backward = Array2::zeros(gradients.dim());
392    let gradients_backward_view = gradients_backward.view();
393
394    reconstruct_hessian_central_diff(gradients, &gradients_backward_view, p, sparsity, h)
395}