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                .unwrap()
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().unwrap();
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                    .unwrap()
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
311            .blocks
312            .sort_by(|a, b| b.priority.partial_cmp(&a.priority).unwrap());
313
314        // Process blocks in order of priority
315        let num_blocks = state.blocks.len();
316        for i in 0..num_blocks {
317            iteration += 1;
318
319            // Extract current block variables
320            let current_x = state.get_variables()?;
321            let block_start = state.blocks[i].start_idx;
322            let block_size = state.blocks[i].size;
323            let block_x = current_x
324                .slice(scirs2_core::ndarray::s![
325                    block_start..block_start + block_size
326                ])
327                .to_owned();
328
329            // Optimize this block while keeping others fixed
330            let block_result = optimize_block(
331                &fun,
332                &current_x,
333                block_start,
334                block_x,
335                &options.memory_options,
336            )?;
337
338            total_nfev += block_result.nfev;
339
340            // Update the full solution with optimized block
341            let mut new_x = current_x.clone();
342            new_x
343                .slice_mut(scirs2_core::ndarray::s![
344                    block_start..block_start + block_size
345                ])
346                .assign(&block_result.x);
347
348            state.update_variables(&new_x)?;
349            state.blocks[i].last_updated = iteration;
350
351            // Check global convergence periodically
352            if iteration % 10 == 0 {
353                let f_val = fun(&new_x.view()).into();
354                let grad_norm = estimate_sparse_gradient_norm(&fun, &new_x.view(), options)?;
355
356                if grad_norm < options.memory_options.base_options.gtol {
357                    println!("Converged at pass {}, iteration {}", pass + 1, iteration);
358
359                    return Ok(OptimizeResult {
360                        x: new_x.clone(),
361                        fun: fun(&new_x.view()),
362                        nit: iteration,
363                        func_evals: total_nfev,
364                        nfev: total_nfev,
365                        success: true,
366                        message: "Advanced-scale optimization converged successfully.".to_string(),
367                        jacobian: None,
368                        hessian: None,
369                    });
370                }
371
372                println!(
373                    "Iteration {}: f = {:.6e}, ||g|| ≈ {:.6e}",
374                    iteration, f_val, grad_norm
375                );
376            }
377
378            if iteration >= options.memory_options.base_options.max_iter {
379                break;
380            }
381        }
382    }
383
384    let final_x = state.get_variables()?;
385
386    Ok(OptimizeResult {
387        x: final_x.clone(),
388        fun: fun(&final_x.view()),
389        nit: iteration,
390        func_evals: total_nfev,
391        nfev: total_nfev,
392        success: iteration < options.memory_options.base_options.max_iter,
393        message: if iteration < options.memory_options.base_options.max_iter {
394            "Advanced-scale optimization completed successfully.".to_string()
395        } else {
396            "Maximum iterations reached.".to_string()
397        },
398        jacobian: None,
399        hessian: None,
400    })
401}
402
403/// Optimize a single block of variables
404#[allow(dead_code)]
405fn optimize_block<F, S>(
406    fun: &F,
407    full_x: &Array1<f64>,
408    block_start: usize,
409    block_x0: Array1<f64>,
410    options: &MemoryOptions,
411) -> Result<OptimizeResult<f64>, OptimizeError>
412where
413    F: Fn(&ArrayView1<f64>) -> S,
414    S: Into<f64> + Clone,
415{
416    // Create a wrapper function that only varies the block variables
417    let block_fun = |block_vars: &ArrayView1<f64>| -> f64 {
418        let mut temp_x = full_x.clone();
419        temp_x
420            .slice_mut(scirs2_core::ndarray::s![
421                block_start..block_start + block_vars.len()
422            ])
423            .assign(block_vars);
424        fun(&temp_x.view()).into()
425    };
426
427    // Use memory-efficient L-BFGS for the block
428    super::memory_efficient::minimize_memory_efficient_lbfgs(block_fun, block_x0, options)
429}
430
431/// Update block priorities based on gradient magnitude
432#[allow(dead_code)]
433fn update_block_priorities(blocks: &mut [VariableBlock], gradient: &CsrArray<f64>) {
434    for block in blocks {
435        // Compute average gradient magnitude in this block
436        let mut total_grad_mag = 0.0;
437        let mut count = 0;
438
439        // Sparse gradient access
440        for idx in block.start_idx..block.start_idx + block.size {
441            if let Some(&grad_val) = gradient.get_data().get(idx) {
442                total_grad_mag += grad_val.abs();
443                count += 1;
444            }
445        }
446
447        if count > 0 {
448            block.priority = total_grad_mag / count as f64;
449        } else {
450            block.priority *= 0.9; // Decay priority for _blocks with no gradient info
451        }
452    }
453}
454
455/// Estimate sparse gradient norm efficiently
456#[allow(dead_code)]
457fn estimate_sparse_gradient_norm<F, S>(
458    fun: &F,
459    x: &ArrayView1<f64>,
460    options: &AdvancedScaleOptions,
461) -> Result<f64, OptimizeError>
462where
463    F: Fn(&ArrayView1<f64>) -> S,
464    S: Into<f64>,
465{
466    let n = x.len();
467
468    // For very large problems, use sampling to estimate gradient norm
469    if n > options.max_variables_in_memory {
470        // Sample a subset of variables for gradient estimation
471        let sample_size = (options.max_variables_in_memory / 10).min(1000).max(10);
472        let step_size = options.sparse_options.rel_step.unwrap_or(1e-8);
473
474        let mut gradient_norm_squared = 0.0;
475        let f0 = fun(x).into();
476
477        // Sample variables uniformly across the space
478        let step = n / sample_size;
479        for i in (0..n).step_by(step).take(sample_size) {
480            // Compute finite difference for this variable
481            let mut x_pert = x.to_owned();
482            let h = step_size * (1.0 + x[i].abs());
483            x_pert[i] += h;
484
485            let f_pert = fun(&x_pert.view()).into();
486            let grad_i = (f_pert - f0) / h;
487            gradient_norm_squared += grad_i * grad_i;
488        }
489
490        // Scale by the sampling ratio to estimate full gradient norm
491        let scaling_factor = n as f64 / sample_size as f64;
492        Ok((gradient_norm_squared * scaling_factor).sqrt())
493    } else {
494        // For smaller problems, compute a more accurate sparse gradient
495        compute_sparse_gradient_norm(fun, x, &options.sparse_options)
496    }
497}
498
499/// Compute sparse gradient norm using finite differences
500#[allow(dead_code)]
501fn compute_sparse_gradient_norm<F, S>(
502    fun: &F,
503    x: &ArrayView1<f64>,
504    sparse_options: &SparseFiniteDiffOptions,
505) -> Result<f64, OptimizeError>
506where
507    F: Fn(&ArrayView1<f64>) -> S,
508    S: Into<f64>,
509{
510    let n = x.len();
511    let step_size = sparse_options.rel_step.unwrap_or(1e-8);
512    let _f0 = fun(x).into();
513
514    // Use central differences for better accuracy
515    let mut gradient_norm_squared = 0.0;
516
517    for i in 0..n {
518        let h = step_size * (1.0 + x[i].abs());
519
520        // Forward difference
521        let mut x_forward = x.to_owned();
522        x_forward[i] += h;
523        let f_forward = fun(&x_forward.view()).into();
524
525        // Backward difference
526        let mut x_backward = x.to_owned();
527        x_backward[i] -= h;
528        let f_backward = fun(&x_backward.view()).into();
529
530        // Central difference
531        let grad_i = (f_forward - f_backward) / (2.0 * h);
532        gradient_norm_squared += grad_i * grad_i;
533    }
534
535    Ok(gradient_norm_squared.sqrt())
536}
537
538/// Create advanced-scale optimizer with automatic parameter selection
539#[allow(dead_code)]
540pub fn create_advanced_scale_optimizer(
541    problem_size: usize,
542    available_memory_mb: usize,
543    estimated_sparsity: f64, // Fraction of non-zero elements
544) -> AdvancedScaleOptions {
545    let available_bytes = available_memory_mb * 1024 * 1024;
546
547    // Estimate memory per variable
548    let bytes_per_var = std::mem::size_of::<f64>() * 8; // Variable + gradient + temporaries
549    let max_vars_in_memory = (available_bytes / bytes_per_var).min(problem_size);
550
551    // Block _size based on memory and _sparsity
552    let block_size = if estimated_sparsity < 0.1 {
553        // Very sparse: larger blocks
554        (max_vars_in_memory / 4).max(1000)
555    } else {
556        // Dense: smaller blocks
557        (max_vars_in_memory / 10).max(100)
558    };
559
560    // Use disk storage for very large problems
561    let use_disk = problem_size >= 1_000_000 || available_memory_mb < 512;
562
563    // More refinement passes for larger problems
564    let refinement_passes = if problem_size > 10_000_000 {
565        5
566    } else if problem_size > 1_000_000 {
567        4
568    } else {
569        3
570    };
571
572    AdvancedScaleOptions {
573        memory_options: super::memory_efficient::create_memory_efficient_optimizer(
574            max_vars_in_memory,
575            available_memory_mb / 2,
576        ),
577        sparse_options: SparseFiniteDiffOptions {
578            max_group_size: (estimated_sparsity * 1000.0) as usize,
579            ..Default::default()
580        },
581        max_variables_in_memory: max_vars_in_memory,
582        block_size,
583        refinement_passes,
584        use_disk_storage: use_disk,
585        mmap_threshold: available_bytes / (2 * std::mem::size_of::<f64>()),
586        compression_level: if available_memory_mb < 256 { 6 } else { 3 },
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593    use approx::assert_abs_diff_eq;
594
595    #[test]
596    fn test_advanced_scale_small_problem() {
597        // Test that small problems fall back to regular memory-efficient method
598        let quadratic = |x: &ArrayView1<f64>| -> f64 { x.mapv(|xi| xi.powi(2)).sum() };
599
600        let n = 50; // Small problem
601        let x0 = Array1::ones(n);
602        let options = AdvancedScaleOptions::default();
603
604        let result = minimize_advanced_scale(quadratic, x0, &options).unwrap();
605
606        assert!(result.success);
607        // Should converge to origin
608        for i in 0..n {
609            assert_abs_diff_eq!(result.x[i], 0.0, epsilon = 1e-3);
610        }
611    }
612
613    #[test]
614    fn test_variable_blocks() {
615        let blocks = create_variable_blocks(1000, 100);
616        assert_eq!(blocks.len(), 10);
617        assert_eq!(blocks[0].start_idx, 0);
618        assert_eq!(blocks[0].size, 100);
619        assert_eq!(blocks[9].start_idx, 900);
620        assert_eq!(blocks[9].size, 100);
621    }
622
623    #[test]
624    fn test_auto_parameter_selection() {
625        let options = create_advanced_scale_optimizer(1_000_000, 1024, 0.05);
626
627        assert!(options.max_variables_in_memory > 0);
628        assert!(options.block_size > 0);
629        assert!(options.refinement_passes >= 3);
630        assert!(options.use_disk_storage); // Should use disk for 1M variables
631    }
632}