scirs2_optimize/unconstrained/
memory_efficient_sparse.rs

1//! Memory-efficient sparse optimization for very large-scale problems
2//!
3//! This module provides optimization algorithms specifically designed for very large
4//! problems (millions of variables) with sparse structure, using out-of-core storage,
5//! progressive refinement, and advanced memory management techniques.
6
7use crate::error::OptimizeError;
8use crate::sparse_numdiff::SparseFiniteDiffOptions;
9use crate::unconstrained::memory_efficient::MemoryOptions;
10use crate::unconstrained::result::OptimizeResult;
11// use crate::unconstrained::sparse_optimization::compute_sparse_gradient;
12use scirs2_core::ndarray::{Array1, ArrayView1};
13use scirs2_sparse::csr_array::CsrArray;
14use std::fs::{File, OpenOptions};
15use std::io::{Read, Seek, SeekFrom, Write};
16use std::path::PathBuf;
17
18/// Options for advanced-large-scale memory-efficient optimization
19#[derive(Debug, Clone)]
20pub struct AdvancedScaleOptions {
21    /// Base memory options
22    pub memory_options: MemoryOptions,
23    /// Sparse finite difference options
24    pub sparse_options: SparseFiniteDiffOptions,
25    /// Maximum number of variables to keep in memory simultaneously
26    pub max_variables_in_memory: usize,
27    /// Block size for progressive refinement
28    pub block_size: usize,
29    /// Number of progressive refinement passes
30    pub refinement_passes: usize,
31    /// Use disk-based storage for very large problems
32    pub use_disk_storage: bool,
33    /// Memory mapping threshold (variables)
34    pub mmap_threshold: usize,
35    /// Compression level for disk storage (0-9)
36    pub compression_level: u32,
37}
38
39impl Default for AdvancedScaleOptions {
40    fn default() -> Self {
41        Self {
42            memory_options: MemoryOptions::default(),
43            sparse_options: SparseFiniteDiffOptions::default(),
44            max_variables_in_memory: 100_000,
45            block_size: 10_000,
46            refinement_passes: 3,
47            use_disk_storage: false,
48            mmap_threshold: 1_000_000,
49            compression_level: 3,
50        }
51    }
52}
53
54/// Advanced-large-scale problem state manager
55struct AdvancedScaleState {
56    /// Current variable values (may be stored on disk)
57    variables: VariableStorage,
58    /// Current gradient (sparse representation)
59    gradient: Option<CsrArray<f64>>,
60    /// Variable blocks for progressive processing
61    blocks: Vec<VariableBlock>,
62    /// Active variable indices for current iteration
63    #[allow(dead_code)]
64    active_variables: Vec<usize>,
65    /// Temporary directory for out-of-core storage
66    #[allow(dead_code)]
67    temp_dir: Option<PathBuf>,
68}
69
70/// Variable storage abstraction (memory or disk-based)
71enum VariableStorage {
72    /// All variables in memory
73    Memory(Array1<f64>),
74    /// Variables stored on disk with memory-mapped access
75    Disk {
76        file_path: PathBuf,
77        size: usize,
78        #[allow(dead_code)]
79        buffer: Vec<f64>, // In-memory cache for active variables
80        #[allow(dead_code)]
81        active_indices: Vec<usize>,
82    },
83}
84
85/// Block of variables for progressive processing
86#[derive(Debug, Clone)]
87struct VariableBlock {
88    /// Starting index of the block
89    start_idx: usize,
90    /// Size of the block
91    size: usize,
92    /// Priority for processing (higher means more important)
93    priority: f64,
94    /// Last time this block was updated
95    last_updated: usize,
96}
97
98impl AdvancedScaleState {
99    fn new(x0: Array1<f64>, options: &AdvancedScaleOptions) -> Result<Self, OptimizeError> {
100        let n = x0.len();
101
102        // Determine storage strategy
103        let variables = if options.use_disk_storage || n > options.mmap_threshold {
104            // Create temporary file for large problems
105            let temp_dir = options
106                .memory_options
107                .temp_dir
108                .clone()
109                .unwrap_or_else(std::env::temp_dir);
110
111            std::fs::create_dir_all(&temp_dir).map_err(|e| {
112                OptimizeError::ComputationError(format!("Failed to create temp directory: {}", e))
113            })?;
114
115            let file_path = temp_dir.join(format!("advancedscale_vars_{}.dat", std::process::id()));
116
117            // Write initial variables to disk
118            let mut file = File::create(&file_path).map_err(|e| {
119                OptimizeError::ComputationError(format!("Failed to create temp file: {}", e))
120            })?;
121
122            let bytes: Vec<u8> = x0
123                .as_slice()
124                .expect("Operation failed")
125                .iter()
126                .flat_map(|&x| x.to_le_bytes())
127                .collect();
128            file.write_all(&bytes).map_err(|e| {
129                OptimizeError::ComputationError(format!("Failed to write to temp file: {}", e))
130            })?;
131
132            VariableStorage::Disk {
133                file_path,
134                size: n,
135                buffer: vec![0.0; options.max_variables_in_memory.min(n)],
136                active_indices: Vec::new(),
137            }
138        } else {
139            VariableStorage::Memory(x0)
140        };
141
142        // Create variable blocks for progressive processing
143        let blocks = create_variable_blocks(n, options.block_size);
144
145        Ok(Self {
146            variables,
147            gradient: None,
148            blocks,
149            active_variables: Vec::new(),
150            temp_dir: options.memory_options.temp_dir.clone(),
151        })
152    }
153
154    /// Get current variable values (loading from disk if necessary)
155    fn get_variables(&mut self) -> Result<Array1<f64>, OptimizeError> {
156        match &mut self.variables {
157            VariableStorage::Memory(x) => Ok(x.clone()),
158            VariableStorage::Disk {
159                file_path,
160                size,
161                buffer: _,
162                active_indices: _,
163            } => {
164                // Load all variables from disk (expensive, use sparingly)
165                let mut file = File::open(file_path).map_err(|e| {
166                    OptimizeError::ComputationError(format!("Failed to open temp file: {}", e))
167                })?;
168
169                let mut bytes = vec![0u8; *size * 8];
170                file.read_exact(&mut bytes).map_err(|e| {
171                    OptimizeError::ComputationError(format!("Failed to read from temp file: {}", e))
172                })?;
173
174                let values: Result<Vec<f64>, _> = bytes
175                    .chunks_exact(8)
176                    .map(|chunk| {
177                        let array: [u8; 8] = chunk.try_into().expect("Operation failed");
178                        Ok(f64::from_le_bytes(array))
179                    })
180                    .collect();
181
182                let values = values.map_err(|_: std::io::Error| {
183                    OptimizeError::ComputationError("Failed to deserialize variables".to_string())
184                })?;
185
186                Ok(Array1::from_vec(values))
187            }
188        }
189    }
190
191    /// Update variables (writing to disk if necessary)
192    fn update_variables(&mut self, new_x: &Array1<f64>) -> Result<(), OptimizeError> {
193        match &mut self.variables {
194            VariableStorage::Memory(x) => {
195                x.assign(new_x);
196                Ok(())
197            }
198            VariableStorage::Disk {
199                file_path,
200                size: _,
201                buffer: _,
202                active_indices: _,
203            } => {
204                // Write all variables to disk
205                let mut file = OpenOptions::new()
206                    .write(true)
207                    .open(file_path)
208                    .map_err(|e| {
209                        OptimizeError::ComputationError(format!("Failed to open temp file: {}", e))
210                    })?;
211
212                let bytes: Vec<u8> = new_x
213                    .as_slice()
214                    .expect("Operation failed")
215                    .iter()
216                    .flat_map(|&x| x.to_le_bytes())
217                    .collect();
218
219                file.seek(SeekFrom::Start(0)).map_err(|e| {
220                    OptimizeError::ComputationError(format!("Failed to seek in temp file: {}", e))
221                })?;
222
223                file.write_all(&bytes).map_err(|e| {
224                    OptimizeError::ComputationError(format!("Failed to write to temp file: {}", e))
225                })?;
226
227                Ok(())
228            }
229        }
230    }
231}
232
233impl Drop for AdvancedScaleState {
234    fn drop(&mut self) {
235        // Clean up temporary files
236        if let VariableStorage::Disk { file_path, .. } = &self.variables {
237            let _ = std::fs::remove_file(file_path);
238        }
239    }
240}
241
242/// Create variable blocks for progressive processing
243#[allow(dead_code)]
244fn create_variable_blocks(n: usize, block_size: usize) -> Vec<VariableBlock> {
245    let mut _blocks = Vec::new();
246    let num_blocks = n.div_ceil(block_size);
247
248    for i in 0..num_blocks {
249        let start_idx = i * block_size;
250        let end_idx = std::cmp::min((i + 1) * block_size, n);
251        let size = end_idx - start_idx;
252
253        _blocks.push(VariableBlock {
254            start_idx,
255            size,
256            priority: 1.0, // Initial priority
257            last_updated: 0,
258        });
259    }
260
261    _blocks
262}
263
264/// Advanced-large-scale optimization using progressive refinement and memory management
265#[allow(dead_code)]
266pub fn minimize_advanced_scale<F, S>(
267    fun: F,
268    x0: Array1<f64>,
269    options: &AdvancedScaleOptions,
270) -> Result<OptimizeResult<S>, OptimizeError>
271where
272    F: Fn(&ArrayView1<f64>) -> S + Clone + Sync,
273    S: Into<f64> + Clone + Send,
274{
275    let n = x0.len();
276
277    if n < options.max_variables_in_memory {
278        // For smaller problems, use regular memory-efficient algorithm
279        return super::memory_efficient::minimize_memory_efficient_lbfgs(
280            fun,
281            x0,
282            &options.memory_options,
283        );
284    }
285
286    println!(
287        "Starting advanced-large-scale optimization for {} variables",
288        n
289    );
290
291    // Initialize state manager
292    let mut state = AdvancedScaleState::new(x0, options)?;
293    let mut iteration = 0;
294    let mut total_nfev = 0;
295
296    // Progressive refinement loop
297    for pass in 0..options.refinement_passes {
298        println!(
299            "Progressive refinement pass {}/{}",
300            pass + 1,
301            options.refinement_passes
302        );
303
304        // Update block priorities based on gradient information
305        if let Some(ref gradient) = state.gradient {
306            update_block_priorities(&mut state.blocks, gradient);
307        }
308
309        // Sort blocks by priority (highest first)
310        state.blocks.sort_by(|a, b| {
311            b.priority
312                .partial_cmp(&a.priority)
313                .expect("Operation failed")
314        });
315
316        // Process blocks in order of priority
317        let num_blocks = state.blocks.len();
318        for i in 0..num_blocks {
319            iteration += 1;
320
321            // Extract current block variables
322            let current_x = state.get_variables()?;
323            let block_start = state.blocks[i].start_idx;
324            let block_size = state.blocks[i].size;
325            let block_x = current_x
326                .slice(scirs2_core::ndarray::s![
327                    block_start..block_start + block_size
328                ])
329                .to_owned();
330
331            // Optimize this block while keeping others fixed
332            let block_result = optimize_block(
333                &fun,
334                &current_x,
335                block_start,
336                block_x,
337                &options.memory_options,
338            )?;
339
340            total_nfev += block_result.nfev;
341
342            // Update the full solution with optimized block
343            let mut new_x = current_x.clone();
344            new_x
345                .slice_mut(scirs2_core::ndarray::s![
346                    block_start..block_start + block_size
347                ])
348                .assign(&block_result.x);
349
350            state.update_variables(&new_x)?;
351            state.blocks[i].last_updated = iteration;
352
353            // Check global convergence periodically
354            if iteration % 10 == 0 {
355                let f_val = fun(&new_x.view()).into();
356                let grad_norm = estimate_sparse_gradient_norm(&fun, &new_x.view(), options)?;
357
358                if grad_norm < options.memory_options.base_options.gtol {
359                    println!("Converged at pass {}, iteration {}", pass + 1, iteration);
360
361                    return Ok(OptimizeResult {
362                        x: new_x.clone(),
363                        fun: fun(&new_x.view()),
364                        nit: iteration,
365                        func_evals: total_nfev,
366                        nfev: total_nfev,
367                        success: true,
368                        message: "Advanced-scale optimization converged successfully.".to_string(),
369                        jacobian: None,
370                        hessian: None,
371                    });
372                }
373
374                println!(
375                    "Iteration {}: f = {:.6e}, ||g|| ≈ {:.6e}",
376                    iteration, f_val, grad_norm
377                );
378            }
379
380            if iteration >= options.memory_options.base_options.max_iter {
381                break;
382            }
383        }
384    }
385
386    let final_x = state.get_variables()?;
387
388    Ok(OptimizeResult {
389        x: final_x.clone(),
390        fun: fun(&final_x.view()),
391        nit: iteration,
392        func_evals: total_nfev,
393        nfev: total_nfev,
394        success: iteration < options.memory_options.base_options.max_iter,
395        message: if iteration < options.memory_options.base_options.max_iter {
396            "Advanced-scale optimization completed successfully.".to_string()
397        } else {
398            "Maximum iterations reached.".to_string()
399        },
400        jacobian: None,
401        hessian: None,
402    })
403}
404
405/// Optimize a single block of variables
406#[allow(dead_code)]
407fn optimize_block<F, S>(
408    fun: &F,
409    full_x: &Array1<f64>,
410    block_start: usize,
411    block_x0: Array1<f64>,
412    options: &MemoryOptions,
413) -> Result<OptimizeResult<f64>, OptimizeError>
414where
415    F: Fn(&ArrayView1<f64>) -> S,
416    S: Into<f64> + Clone,
417{
418    // Create a wrapper function that only varies the block variables
419    let block_fun = |block_vars: &ArrayView1<f64>| -> f64 {
420        let mut temp_x = full_x.clone();
421        temp_x
422            .slice_mut(scirs2_core::ndarray::s![
423                block_start..block_start + block_vars.len()
424            ])
425            .assign(block_vars);
426        fun(&temp_x.view()).into()
427    };
428
429    // Use memory-efficient L-BFGS for the block
430    super::memory_efficient::minimize_memory_efficient_lbfgs(block_fun, block_x0, options)
431}
432
433/// Update block priorities based on gradient magnitude
434#[allow(dead_code)]
435fn update_block_priorities(blocks: &mut [VariableBlock], gradient: &CsrArray<f64>) {
436    for block in blocks {
437        // Compute average gradient magnitude in this block
438        let mut total_grad_mag = 0.0;
439        let mut count = 0;
440
441        // Sparse gradient access
442        for idx in block.start_idx..block.start_idx + block.size {
443            if let Some(&grad_val) = gradient.get_data().get(idx) {
444                total_grad_mag += grad_val.abs();
445                count += 1;
446            }
447        }
448
449        if count > 0 {
450            block.priority = total_grad_mag / count as f64;
451        } else {
452            block.priority *= 0.9; // Decay priority for _blocks with no gradient info
453        }
454    }
455}
456
457/// Estimate sparse gradient norm efficiently
458#[allow(dead_code)]
459fn estimate_sparse_gradient_norm<F, S>(
460    fun: &F,
461    x: &ArrayView1<f64>,
462    options: &AdvancedScaleOptions,
463) -> Result<f64, OptimizeError>
464where
465    F: Fn(&ArrayView1<f64>) -> S,
466    S: Into<f64>,
467{
468    let n = x.len();
469
470    // For very large problems, use sampling to estimate gradient norm
471    if n > options.max_variables_in_memory {
472        // Sample a subset of variables for gradient estimation
473        let sample_size = (options.max_variables_in_memory / 10).min(1000).max(10);
474        let step_size = options.sparse_options.rel_step.unwrap_or(1e-8);
475
476        let mut gradient_norm_squared = 0.0;
477        let f0 = fun(x).into();
478
479        // Sample variables uniformly across the space
480        let step = n / sample_size;
481        for i in (0..n).step_by(step).take(sample_size) {
482            // Compute finite difference for this variable
483            let mut x_pert = x.to_owned();
484            let h = step_size * (1.0 + x[i].abs());
485            x_pert[i] += h;
486
487            let f_pert = fun(&x_pert.view()).into();
488            let grad_i = (f_pert - f0) / h;
489            gradient_norm_squared += grad_i * grad_i;
490        }
491
492        // Scale by the sampling ratio to estimate full gradient norm
493        let scaling_factor = n as f64 / sample_size as f64;
494        Ok((gradient_norm_squared * scaling_factor).sqrt())
495    } else {
496        // For smaller problems, compute a more accurate sparse gradient
497        compute_sparse_gradient_norm(fun, x, &options.sparse_options)
498    }
499}
500
501/// Compute sparse gradient norm using finite differences
502#[allow(dead_code)]
503fn compute_sparse_gradient_norm<F, S>(
504    fun: &F,
505    x: &ArrayView1<f64>,
506    sparse_options: &SparseFiniteDiffOptions,
507) -> Result<f64, OptimizeError>
508where
509    F: Fn(&ArrayView1<f64>) -> S,
510    S: Into<f64>,
511{
512    let n = x.len();
513    let step_size = sparse_options.rel_step.unwrap_or(1e-8);
514    let _f0 = fun(x).into();
515
516    // Use central differences for better accuracy
517    let mut gradient_norm_squared = 0.0;
518
519    for i in 0..n {
520        let h = step_size * (1.0 + x[i].abs());
521
522        // Forward difference
523        let mut x_forward = x.to_owned();
524        x_forward[i] += h;
525        let f_forward = fun(&x_forward.view()).into();
526
527        // Backward difference
528        let mut x_backward = x.to_owned();
529        x_backward[i] -= h;
530        let f_backward = fun(&x_backward.view()).into();
531
532        // Central difference
533        let grad_i = (f_forward - f_backward) / (2.0 * h);
534        gradient_norm_squared += grad_i * grad_i;
535    }
536
537    Ok(gradient_norm_squared.sqrt())
538}
539
540/// Create advanced-scale optimizer with automatic parameter selection
541#[allow(dead_code)]
542pub fn create_advanced_scale_optimizer(
543    problem_size: usize,
544    available_memory_mb: usize,
545    estimated_sparsity: f64, // Fraction of non-zero elements
546) -> AdvancedScaleOptions {
547    let available_bytes = available_memory_mb * 1024 * 1024;
548
549    // Estimate memory per variable
550    let bytes_per_var = std::mem::size_of::<f64>() * 8; // Variable + gradient + temporaries
551    let max_vars_in_memory = (available_bytes / bytes_per_var).min(problem_size);
552
553    // Block _size based on memory and _sparsity
554    let block_size = if estimated_sparsity < 0.1 {
555        // Very sparse: larger blocks
556        (max_vars_in_memory / 4).max(1000)
557    } else {
558        // Dense: smaller blocks
559        (max_vars_in_memory / 10).max(100)
560    };
561
562    // Use disk storage for very large problems
563    let use_disk = problem_size >= 1_000_000 || available_memory_mb < 512;
564
565    // More refinement passes for larger problems
566    let refinement_passes = if problem_size > 10_000_000 {
567        5
568    } else if problem_size > 1_000_000 {
569        4
570    } else {
571        3
572    };
573
574    AdvancedScaleOptions {
575        memory_options: super::memory_efficient::create_memory_efficient_optimizer(
576            max_vars_in_memory,
577            available_memory_mb / 2,
578        ),
579        sparse_options: SparseFiniteDiffOptions {
580            max_group_size: (estimated_sparsity * 1000.0) as usize,
581            ..Default::default()
582        },
583        max_variables_in_memory: max_vars_in_memory,
584        block_size,
585        refinement_passes,
586        use_disk_storage: use_disk,
587        mmap_threshold: available_bytes / (2 * std::mem::size_of::<f64>()),
588        compression_level: if available_memory_mb < 256 { 6 } else { 3 },
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use approx::assert_abs_diff_eq;
596
597    #[test]
598    fn test_advanced_scale_small_problem() {
599        // Test that small problems fall back to regular memory-efficient method
600        let quadratic = |x: &ArrayView1<f64>| -> f64 { x.mapv(|xi| xi.powi(2)).sum() };
601
602        let n = 50; // Small problem
603        let x0 = Array1::ones(n);
604        let options = AdvancedScaleOptions::default();
605
606        let result = minimize_advanced_scale(quadratic, x0, &options).expect("Operation failed");
607
608        assert!(result.success);
609        // Should converge to origin
610        for i in 0..n {
611            assert_abs_diff_eq!(result.x[i], 0.0, epsilon = 1e-3);
612        }
613    }
614
615    #[test]
616    fn test_variable_blocks() {
617        let blocks = create_variable_blocks(1000, 100);
618        assert_eq!(blocks.len(), 10);
619        assert_eq!(blocks[0].start_idx, 0);
620        assert_eq!(blocks[0].size, 100);
621        assert_eq!(blocks[9].start_idx, 900);
622        assert_eq!(blocks[9].size, 100);
623    }
624
625    #[test]
626    fn test_auto_parameter_selection() {
627        let options = create_advanced_scale_optimizer(1_000_000, 1024, 0.05);
628
629        assert!(options.max_variables_in_memory > 0);
630        assert!(options.block_size > 0);
631        assert!(options.refinement_passes >= 3);
632        assert!(options.use_disk_storage); // Should use disk for 1M variables
633    }
634}