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}