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 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 ultra-large-scale memory-efficient optimization
19#[derive(Debug, Clone)]
20pub struct UltraScaleOptions {
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 UltraScaleOptions {
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/// Ultra-large-scale problem state manager
55struct UltraScaleState {
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 UltraScaleState {
99    fn new(x0: Array1<f64>, options: &UltraScaleOptions) -> 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!("ultrascale_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 UltraScaleState {
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
243fn create_variable_blocks(n: usize, block_size: usize) -> Vec<VariableBlock> {
244    let mut blocks = Vec::new();
245    let num_blocks = n.div_ceil(block_size);
246
247    for i in 0..num_blocks {
248        let start_idx = i * block_size;
249        let end_idx = std::cmp::min((i + 1) * block_size, n);
250        let size = end_idx - start_idx;
251
252        blocks.push(VariableBlock {
253            start_idx,
254            size,
255            priority: 1.0, // Initial priority
256            last_updated: 0,
257        });
258    }
259
260    blocks
261}
262
263/// Ultra-large-scale optimization using progressive refinement and memory management
264pub fn minimize_ultra_scale<F, S>(
265    fun: F,
266    x0: Array1<f64>,
267    options: &UltraScaleOptions,
268) -> Result<OptimizeResult<S>, OptimizeError>
269where
270    F: Fn(&ArrayView1<f64>) -> S + Clone + Sync,
271    S: Into<f64> + Clone + Send,
272{
273    let n = x0.len();
274
275    if n < options.max_variables_in_memory {
276        // For smaller problems, use regular memory-efficient algorithm
277        return super::memory_efficient::minimize_memory_efficient_lbfgs(
278            fun,
279            x0,
280            &options.memory_options,
281        );
282    }
283
284    println!(
285        "Starting ultra-large-scale optimization for {} variables",
286        n
287    );
288
289    // Initialize state manager
290    let mut state = UltraScaleState::new(x0, options)?;
291    let mut iteration = 0;
292    let mut total_nfev = 0;
293
294    // Progressive refinement loop
295    for pass in 0..options.refinement_passes {
296        println!(
297            "Progressive refinement pass {}/{}",
298            pass + 1,
299            options.refinement_passes
300        );
301
302        // Update block priorities based on gradient information
303        if let Some(ref gradient) = state.gradient {
304            update_block_priorities(&mut state.blocks, gradient);
305        }
306
307        // Sort blocks by priority (highest first)
308        state
309            .blocks
310            .sort_by(|a, b| b.priority.partial_cmp(&a.priority).unwrap());
311
312        // Process blocks in order of priority
313        let num_blocks = state.blocks.len();
314        for i in 0..num_blocks {
315            iteration += 1;
316
317            // Extract current block variables
318            let current_x = state.get_variables()?;
319            let block_start = state.blocks[i].start_idx;
320            let block_size = state.blocks[i].size;
321            let block_x = current_x
322                .slice(ndarray::s![block_start..block_start + block_size])
323                .to_owned();
324
325            // Optimize this block while keeping others fixed
326            let block_result = optimize_block(
327                &fun,
328                &current_x,
329                block_start,
330                block_x,
331                &options.memory_options,
332            )?;
333
334            total_nfev += block_result.nfev;
335
336            // Update the full solution with optimized block
337            let mut new_x = current_x.clone();
338            new_x
339                .slice_mut(ndarray::s![block_start..block_start + block_size])
340                .assign(&block_result.x);
341
342            state.update_variables(&new_x)?;
343            state.blocks[i].last_updated = iteration;
344
345            // Check global convergence periodically
346            if iteration % 10 == 0 {
347                let f_val = fun(&new_x.view()).into();
348                let grad_norm = estimate_sparse_gradient_norm(&fun, &new_x.view(), options)?;
349
350                if grad_norm < options.memory_options.base_options.gtol {
351                    println!("Converged at pass {}, iteration {}", pass + 1, iteration);
352
353                    return Ok(OptimizeResult {
354                        x: new_x.clone(),
355                        fun: fun(&new_x.view()),
356                        iterations: iteration,
357                        nit: iteration,
358                        func_evals: total_nfev,
359                        nfev: total_nfev,
360                        success: true,
361                        message: "Ultra-scale optimization converged successfully.".to_string(),
362                        jacobian: None,
363                        hessian: None,
364                    });
365                }
366
367                println!(
368                    "Iteration {}: f = {:.6e}, ||g|| ≈ {:.6e}",
369                    iteration, f_val, grad_norm
370                );
371            }
372
373            if iteration >= options.memory_options.base_options.max_iter {
374                break;
375            }
376        }
377    }
378
379    let final_x = state.get_variables()?;
380
381    Ok(OptimizeResult {
382        x: final_x.clone(),
383        fun: fun(&final_x.view()),
384        iterations: iteration,
385        nit: iteration,
386        func_evals: total_nfev,
387        nfev: total_nfev,
388        success: iteration < options.memory_options.base_options.max_iter,
389        message: if iteration < options.memory_options.base_options.max_iter {
390            "Ultra-scale optimization completed successfully.".to_string()
391        } else {
392            "Maximum iterations reached.".to_string()
393        },
394        jacobian: None,
395        hessian: None,
396    })
397}
398
399/// Optimize a single block of variables
400fn optimize_block<F, S>(
401    fun: &F,
402    full_x: &Array1<f64>,
403    block_start: usize,
404    block_x0: Array1<f64>,
405    options: &MemoryOptions,
406) -> Result<OptimizeResult<f64>, OptimizeError>
407where
408    F: Fn(&ArrayView1<f64>) -> S,
409    S: Into<f64> + Clone,
410{
411    // Create a wrapper function that only varies the block variables
412    let block_fun = |block_vars: &ArrayView1<f64>| -> f64 {
413        let mut temp_x = full_x.clone();
414        temp_x
415            .slice_mut(ndarray::s![block_start..block_start + block_vars.len()])
416            .assign(block_vars);
417        fun(&temp_x.view()).into()
418    };
419
420    // Use memory-efficient L-BFGS for the block
421    super::memory_efficient::minimize_memory_efficient_lbfgs(block_fun, block_x0, options)
422}
423
424/// Update block priorities based on gradient magnitude
425fn update_block_priorities(blocks: &mut [VariableBlock], gradient: &CsrArray<f64>) {
426    for block in blocks {
427        // Compute average gradient magnitude in this block
428        let mut total_grad_mag = 0.0;
429        let mut count = 0;
430
431        // Sparse gradient access
432        for idx in block.start_idx..block.start_idx + block.size {
433            if let Some(&grad_val) = gradient.get_data().get(idx) {
434                total_grad_mag += grad_val.abs();
435                count += 1;
436            }
437        }
438
439        if count > 0 {
440            block.priority = total_grad_mag / count as f64;
441        } else {
442            block.priority *= 0.9; // Decay priority for blocks with no gradient info
443        }
444    }
445}
446
447/// Estimate sparse gradient norm efficiently
448fn estimate_sparse_gradient_norm<F, S>(
449    fun: &F,
450    x: &ArrayView1<f64>,
451    options: &UltraScaleOptions,
452) -> Result<f64, OptimizeError>
453where
454    F: Fn(&ArrayView1<f64>) -> S,
455    S: Into<f64>,
456{
457    // TODO: Implement sparse gradient computation when function is available
458    // For now, return a placeholder gradient norm
459    let _sparse_options = &options.sparse_options;
460    let _fun_val = fun(x).into();
461    Ok(1e-6) // Placeholder gradient norm
462}
463
464/// Create ultra-scale optimizer with automatic parameter selection
465pub fn create_ultra_scale_optimizer(
466    problem_size: usize,
467    available_memory_mb: usize,
468    estimated_sparsity: f64, // Fraction of non-zero elements
469) -> UltraScaleOptions {
470    let available_bytes = available_memory_mb * 1024 * 1024;
471
472    // Estimate memory per variable
473    let bytes_per_var = std::mem::size_of::<f64>() * 8; // Variable + gradient + temporaries
474    let max_vars_in_memory = (available_bytes / bytes_per_var).min(problem_size);
475
476    // Block size based on memory and sparsity
477    let block_size = if estimated_sparsity < 0.1 {
478        // Very sparse: larger blocks
479        (max_vars_in_memory / 4).max(1000)
480    } else {
481        // Dense: smaller blocks
482        (max_vars_in_memory / 10).max(100)
483    };
484
485    // Use disk storage for very large problems
486    let use_disk = problem_size >= 1_000_000 || available_memory_mb < 512;
487
488    // More refinement passes for larger problems
489    let refinement_passes = if problem_size > 10_000_000 {
490        5
491    } else if problem_size > 1_000_000 {
492        4
493    } else {
494        3
495    };
496
497    UltraScaleOptions {
498        memory_options: super::memory_efficient::create_memory_efficient_optimizer(
499            max_vars_in_memory,
500            available_memory_mb / 2,
501        ),
502        sparse_options: SparseFiniteDiffOptions {
503            max_group_size: (estimated_sparsity * 1000.0) as usize,
504            ..Default::default()
505        },
506        max_variables_in_memory: max_vars_in_memory,
507        block_size,
508        refinement_passes,
509        use_disk_storage: use_disk,
510        mmap_threshold: available_bytes / (2 * std::mem::size_of::<f64>()),
511        compression_level: if available_memory_mb < 256 { 6 } else { 3 },
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use approx::assert_abs_diff_eq;
519
520    #[test]
521    fn test_ultra_scale_small_problem() {
522        // Test that small problems fall back to regular memory-efficient method
523        let quadratic = |x: &ArrayView1<f64>| -> f64 { x.mapv(|xi| xi.powi(2)).sum() };
524
525        let n = 50; // Small problem
526        let x0 = Array1::ones(n);
527        let options = UltraScaleOptions::default();
528
529        let result = minimize_ultra_scale(quadratic, x0, &options).unwrap();
530
531        assert!(result.success);
532        // Should converge to origin
533        for i in 0..n {
534            assert_abs_diff_eq!(result.x[i], 0.0, epsilon = 1e-3);
535        }
536    }
537
538    #[test]
539    fn test_variable_blocks() {
540        let blocks = create_variable_blocks(1000, 100);
541        assert_eq!(blocks.len(), 10);
542        assert_eq!(blocks[0].start_idx, 0);
543        assert_eq!(blocks[0].size, 100);
544        assert_eq!(blocks[9].start_idx, 900);
545        assert_eq!(blocks[9].size, 100);
546    }
547
548    #[test]
549    fn test_auto_parameter_selection() {
550        let options = create_ultra_scale_optimizer(1_000_000, 1024, 0.05);
551
552        assert!(options.max_variables_in_memory > 0);
553        assert!(options.block_size > 0);
554        assert!(options.refinement_passes >= 3);
555        assert!(options.use_disk_storage); // Should use disk for 1M variables
556    }
557}