Skip to main content

scirs2_optimize/unconstrained/
mod.rs

1//! Unconstrained optimization algorithms
2//!
3//! This module provides various algorithms for unconstrained minimization problems.
4
5use crate::error::OptimizeError;
6use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
7use std::fmt;
8
9/// Method for computing the Jacobian (gradient) of the objective function
10pub enum Jacobian<'a> {
11    /// Compute gradient using finite differences
12    FiniteDiff,
13    /// User-provided gradient function
14    Function(Box<dyn Fn(&ArrayView1<f64>) -> Array1<f64> + 'a>),
15}
16
17impl<'a> std::fmt::Debug for Jacobian<'a> {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            Jacobian::FiniteDiff => write!(f, "Jacobian::FiniteDiff"),
21            Jacobian::Function(_) => write!(f, "Jacobian::Function(<function>)"),
22        }
23    }
24}
25
26// Sub-modules
27pub mod adaptive_convergence;
28pub mod advanced_line_search;
29pub mod bfgs;
30pub mod callback_diagnostics;
31pub mod conjugate_gradient;
32pub mod convergence_diagnostics;
33pub mod efficient_sparse;
34pub mod lbfgs;
35pub mod line_search;
36pub mod memory_efficient;
37pub mod memory_efficient_sparse;
38pub mod nelder_mead;
39pub mod newton;
40pub mod powell;
41pub mod quasi_newton;
42pub mod result;
43pub mod robust_convergence;
44pub mod simd_bfgs;
45pub mod sparse_optimization;
46pub mod strong_wolfe;
47pub mod subspace_methods;
48pub mod truncated_newton;
49pub mod trust_region;
50pub mod utils;
51
52// Import result type
53pub use result::OptimizeResult;
54
55// Re-export commonly used items
56pub use adaptive_convergence::{
57    check_convergence_adaptive, create_adaptive_options_for_problem, AdaptationStats,
58    AdaptiveToleranceOptions, AdaptiveToleranceState, ConvergenceStatus,
59};
60pub use advanced_line_search::{
61    advanced_line_search, create_non_monotone_state, AdvancedLineSearchOptions,
62    InterpolationStrategy, LineSearchMethod, LineSearchResult, LineSearchStats,
63};
64pub use bfgs::{minimize_bfgs, minimize_bfgs_no_grad, minimize_bfgs_with_jacobian};
65pub use callback_diagnostics::{
66    minimize_with_diagnostics, optimize_with_diagnostics, CallbackInfo, CallbackResult,
67    DiagnosticOptimizer, OptimizationCallback,
68};
69pub use conjugate_gradient::{
70    minimize_conjugate_gradient, minimize_conjugate_gradient_with_jacobian,
71};
72pub use convergence_diagnostics::{
73    ConvergenceDiagnostics, DiagnosticCollector, DiagnosticOptions, DiagnosticWarning,
74    ExportFormat, IterationDiagnostic, LineSearchDiagnostic, PerformanceMetrics, ProblemAnalysis,
75    ProblemDifficulty, WarningSeverity,
76};
77pub use efficient_sparse::{
78    minimize_efficient_sparse_newton, EfficientSparseOptions, SparsityInfo,
79};
80pub use lbfgs::{minimize_lbfgs, minimize_lbfgsb};
81pub use memory_efficient::{
82    create_memory_efficient_optimizer, minimize_memory_efficient_lbfgs, MemoryOptions,
83};
84pub use memory_efficient_sparse::{
85    create_advanced_scale_optimizer, minimize_advanced_scale, AdvancedScaleOptions,
86};
87pub use nelder_mead::minimize_nelder_mead;
88pub use newton::minimize_newton_cg;
89pub use powell::minimize_powell;
90pub use quasi_newton::{minimize_dfp, minimize_quasi_newton, minimize_sr1, UpdateFormula};
91pub use robust_convergence::{
92    create_robust_options_for_problem, RobustConvergenceOptions, RobustConvergenceResult,
93    RobustConvergenceState,
94};
95pub use simd_bfgs::{minimize_simd_bfgs, minimize_simd_bfgs_default, SimdBfgsOptions};
96pub use sparse_optimization::{
97    auto_detect_sparsity, minimize_sparse_bfgs, SparseOptimizationOptions,
98};
99pub use strong_wolfe::{
100    create_strong_wolfe_options_for_method, strong_wolfe_line_search, StrongWolfeOptions,
101    StrongWolfeResult,
102};
103pub use subspace_methods::{
104    minimize_adaptive_subspace, minimize_block_coordinate_descent,
105    minimize_cyclical_coordinate_descent, minimize_random_coordinate_descent,
106    minimize_random_subspace, minimize_subspace, SubspaceMethod, SubspaceOptions,
107};
108pub use truncated_newton::{
109    minimize_truncated_newton, minimize_trust_region_newton, Preconditioner, TruncatedNewtonOptions,
110};
111pub use trust_region::{
112    cauchy_point, dogleg_step, minimize_trust_exact, minimize_trust_krylov, minimize_trust_ncg,
113    solve_trust_subproblem, trust_region_minimize, TrustRegionConfig, TrustRegionResult,
114};
115
116/// Optimization methods for unconstrained minimization.
117#[derive(Debug, Clone, Copy)]
118pub enum Method {
119    /// Nelder-Mead simplex method
120    NelderMead,
121    /// Powell's method
122    Powell,
123    /// Conjugate gradient method
124    CG,
125    /// BFGS quasi-Newton method
126    BFGS,
127    /// SR1 quasi-Newton method
128    SR1,
129    /// DFP quasi-Newton method
130    DFP,
131    /// Limited-memory BFGS method
132    LBFGS,
133    /// Limited-memory BFGS method with bounds support
134    LBFGSB,
135    /// Newton's method with conjugate gradient solver
136    NewtonCG,
137    /// Trust-region Newton method with conjugate gradient solver
138    TrustNCG,
139    /// Trust-region method with Krylov subproblem solver
140    TrustKrylov,
141    /// Trust-region method with exact subproblem solver
142    TrustExact,
143    /// Truncated Newton method
144    TruncatedNewton,
145    /// Trust-region Newton method with truncated CG
146    TrustRegionNewton,
147    /// Trust-region dogleg method (Cauchy point + dogleg step)
148    TrustRegionDogleg,
149}
150
151/// Bounds for optimization variables
152#[derive(Debug, Clone)]
153pub struct Bounds {
154    /// Lower bounds
155    pub lower: Vec<Option<f64>>,
156    /// Upper bounds
157    pub upper: Vec<Option<f64>>,
158}
159
160/// Options for optimization algorithms
161#[derive(Debug, Clone)]
162pub struct Options {
163    /// Maximum number of iterations
164    pub max_iter: usize,
165    /// Maximum number of function evaluations
166    pub max_fev: Option<usize>,
167    /// Function tolerance for convergence
168    pub ftol: f64,
169    /// Change tolerance for convergence
170    pub xtol: f64,
171    /// Gradient tolerance for convergence
172    pub gtol: f64,
173    /// Initial step size
174    pub initial_step: Option<f64>,
175    /// Maximum step size for line search
176    pub maxstep: Option<f64>,
177    /// Whether to use finite differences for gradient
178    pub finite_diff: bool,
179    /// Finite difference step size
180    pub eps: f64,
181    /// Initial trust-region radius for trust-region methods
182    pub trust_radius: Option<f64>,
183    /// Maximum trust-region radius for trust-region methods
184    pub max_trust_radius: Option<f64>,
185    /// Minimum trust-region radius for trust-region methods
186    pub min_trust_radius: Option<f64>,
187    /// Tolerance for the trust-region subproblem
188    pub trust_tol: Option<f64>,
189    /// Maximum iterations for trust-region subproblem
190    pub trust_max_iter: Option<usize>,
191    /// Threshold for accepting a step in the trust-region method
192    pub trust_eta: Option<f64>,
193    /// Bounds constraints for variables
194    pub bounds: Option<Bounds>,
195}
196
197// Implement Display for Method
198impl fmt::Display for Method {
199    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
200        match self {
201            Method::NelderMead => write!(f, "Nelder-Mead"),
202            Method::Powell => write!(f, "Powell"),
203            Method::CG => write!(f, "Conjugate Gradient"),
204            Method::BFGS => write!(f, "BFGS"),
205            Method::SR1 => write!(f, "SR1"),
206            Method::DFP => write!(f, "DFP"),
207            Method::LBFGS => write!(f, "L-BFGS"),
208            Method::LBFGSB => write!(f, "L-BFGS-B"),
209            Method::NewtonCG => write!(f, "Newton-CG"),
210            Method::TrustNCG => write!(f, "Trust-NCG"),
211            Method::TrustKrylov => write!(f, "Trust-Krylov"),
212            Method::TrustExact => write!(f, "Trust-Exact"),
213            Method::TruncatedNewton => write!(f, "Truncated Newton"),
214            Method::TrustRegionNewton => write!(f, "Trust-Region Newton"),
215            Method::TrustRegionDogleg => write!(f, "Trust-Region Dogleg"),
216        }
217    }
218}
219
220// Implement Default for Options
221impl Default for Options {
222    fn default() -> Self {
223        Options {
224            max_iter: 1000,
225            max_fev: None,
226            ftol: 1e-8,
227            xtol: 1e-8,
228            gtol: 1e-5,
229            initial_step: None,
230            maxstep: None,
231            finite_diff: false,
232            eps: 1.4901161193847656e-8,
233            trust_radius: Some(1.0),
234            max_trust_radius: Some(100.0),
235            min_trust_radius: Some(1e-10),
236            trust_tol: Some(1e-8),
237            trust_max_iter: Some(100),
238            trust_eta: Some(0.1),
239            bounds: None,
240        }
241    }
242}
243
244// Implement Bounds methods
245impl Bounds {
246    /// Create new bounds from arrays
247    pub fn new(bounds: &[(Option<f64>, Option<f64>)]) -> Self {
248        let (lower, upper): (Vec<_>, Vec<_>) = bounds.iter().cloned().unzip();
249        Self { lower, upper }
250    }
251
252    /// Create bounds from vectors
253    pub fn from_vecs(lb: Vec<Option<f64>>, ub: Vec<Option<f64>>) -> Result<Self, OptimizeError> {
254        if lb.len() != ub.len() {
255            return Err(OptimizeError::ValueError(
256                "Lower and upper bounds must have the same length".to_string(),
257            ));
258        }
259
260        for (l, u) in lb.iter().zip(ub.iter()) {
261            if let (Some(l_val), Some(u_val)) = (l, u) {
262                if l_val > u_val {
263                    return Err(OptimizeError::ValueError(
264                        "Lower bound must be less than or equal to upper bound".to_string(),
265                    ));
266                }
267            }
268        }
269
270        Ok(Self {
271            lower: lb,
272            upper: ub,
273        })
274    }
275
276    /// Check if point is feasible
277    pub fn is_feasible(&self, x: &[f64]) -> bool {
278        if x.len() != self.lower.len() {
279            return false;
280        }
281
282        for (&xi, (&lb, &ub)) in x.iter().zip(self.lower.iter().zip(self.upper.iter())) {
283            if let Some(l) = lb {
284                if xi < l {
285                    return false;
286                }
287            }
288            if let Some(u) = ub {
289                if xi > u {
290                    return false;
291                }
292            }
293        }
294        true
295    }
296
297    /// Project point onto feasible region
298    pub fn project(&self, x: &mut [f64]) {
299        for (xi, (&lb, &ub)) in x.iter_mut().zip(self.lower.iter().zip(self.upper.iter())) {
300            if let Some(l) = lb {
301                if *xi < l {
302                    *xi = l;
303                }
304            }
305            if let Some(u) = ub {
306                if *xi > u {
307                    *xi = u;
308                }
309            }
310        }
311    }
312
313    /// Check if bounds are active
314    pub fn has_bounds(&self) -> bool {
315        self.lower.iter().any(|b| b.is_some()) || self.upper.iter().any(|b| b.is_some())
316    }
317}
318
319/// Main minimize function for unconstrained optimization
320#[allow(dead_code)]
321pub fn minimize<F, S>(
322    fun: F,
323    x0: &[f64],
324    method: Method,
325    options: Option<Options>,
326) -> Result<OptimizeResult<S>, OptimizeError>
327where
328    F: FnMut(&ArrayView1<f64>) -> S + Clone,
329    S: Into<f64> + Clone + From<f64>,
330{
331    let options = &options.unwrap_or_default();
332    let x0 = Array1::from_vec(x0.to_vec());
333
334    // Check initial point feasibility if bounds are provided
335    if let Some(ref bounds) = options.bounds {
336        let x0_slice = x0.as_slice().ok_or_else(|| {
337            OptimizeError::ComputationError("Failed to get slice for feasibility check".to_string())
338        })?;
339        if !bounds.is_feasible(x0_slice) {
340            return Err(OptimizeError::ValueError(
341                "Initial point is not feasible".to_string(),
342            ));
343        }
344    }
345
346    match method {
347        Method::NelderMead => nelder_mead::minimize_nelder_mead(fun, x0, options),
348        Method::Powell => powell::minimize_powell(fun, x0, options),
349        Method::CG => conjugate_gradient::minimize_conjugate_gradient(
350            fun,
351            None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
352            x0,
353            options,
354        ),
355        Method::BFGS => bfgs::minimize_bfgs(
356            fun,
357            None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
358            x0,
359            options,
360        ),
361        Method::SR1 => quasi_newton::minimize_sr1(fun, x0, options),
362        Method::DFP => quasi_newton::minimize_dfp(fun, x0, options),
363        Method::LBFGS => lbfgs::minimize_lbfgs(
364            fun,
365            None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
366            x0,
367            options,
368        ),
369        Method::LBFGSB => lbfgs::minimize_lbfgsb(
370            fun,
371            None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
372            x0,
373            options,
374        ),
375        Method::NewtonCG => newton::minimize_newton_cg(fun, x0, options),
376        Method::TrustNCG => trust_region::minimize_trust_ncg(fun, x0, options),
377        Method::TrustKrylov => trust_region::minimize_trust_krylov(fun, x0, options),
378        Method::TrustExact => trust_region::minimize_trust_exact(fun, x0, options),
379        Method::TruncatedNewton => truncated_newton_wrapper(fun, x0, options),
380        Method::TrustRegionNewton => trust_region_newton_wrapper(fun, x0, options),
381        Method::TrustRegionDogleg => trust_region_dogleg_wrapper(fun, x0, options),
382    }
383}
384
385/// Wrapper function for truncated Newton method
386#[allow(dead_code)]
387fn truncated_newton_wrapper<F, S>(
388    mut fun: F,
389    x0: Array1<f64>,
390    options: &Options,
391) -> Result<OptimizeResult<S>, OptimizeError>
392where
393    F: FnMut(&ArrayView1<f64>) -> S + Clone,
394    S: Into<f64> + Clone + From<f64>,
395{
396    let fun_f64 = move |x: &ArrayView1<f64>| fun(x).into();
397
398    let truncated_options = TruncatedNewtonOptions {
399        max_iter: options.max_iter,
400        tol: options.gtol,
401        max_cg_iter: options.trust_max_iter.unwrap_or(100),
402        cg_tol: options.trust_tol.unwrap_or(0.1),
403        ..Default::default()
404    };
405
406    // Convert result back to generic type
407    let result = truncated_newton::minimize_truncated_newton(
408        fun_f64,
409        None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
410        x0,
411        Some(truncated_options),
412    )?;
413
414    Ok(OptimizeResult {
415        x: result.x,
416        fun: result.fun.into(),
417        nit: result.nit,
418        func_evals: result.func_evals,
419        nfev: result.nfev,
420        jacobian: result.jacobian,
421        hessian: result.hessian,
422        success: result.success,
423        message: result.message,
424    })
425}
426
427/// Wrapper function for trust-region Newton method
428#[allow(dead_code)]
429fn trust_region_newton_wrapper<F, S>(
430    mut fun: F,
431    x0: Array1<f64>,
432    options: &Options,
433) -> Result<OptimizeResult<S>, OptimizeError>
434where
435    F: FnMut(&ArrayView1<f64>) -> S + Clone,
436    S: Into<f64> + Clone + From<f64>,
437{
438    let fun_f64 = move |x: &ArrayView1<f64>| fun(x).into();
439
440    let truncated_options = TruncatedNewtonOptions {
441        max_iter: options.max_iter,
442        tol: options.gtol,
443        max_cg_iter: options.trust_max_iter.unwrap_or(100),
444        cg_tol: options.trust_tol.unwrap_or(0.1),
445        trust_radius: options.trust_radius,
446        ..Default::default()
447    };
448
449    // Convert result back to generic type
450    let result = truncated_newton::minimize_trust_region_newton(
451        fun_f64,
452        None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
453        x0,
454        Some(truncated_options),
455    )?;
456
457    Ok(OptimizeResult {
458        x: result.x,
459        fun: result.fun.into(),
460        nit: result.nit,
461        func_evals: result.func_evals,
462        nfev: result.nfev,
463        jacobian: result.jacobian,
464        hessian: result.hessian,
465        success: result.success,
466        message: result.message,
467    })
468}
469
470/// Wrapper function for trust-region dogleg method
471#[allow(dead_code)]
472fn trust_region_dogleg_wrapper<F, S>(
473    mut fun: F,
474    x0: Array1<f64>,
475    options: &Options,
476) -> Result<OptimizeResult<S>, OptimizeError>
477where
478    F: FnMut(&ArrayView1<f64>) -> S + Clone,
479    S: Into<f64> + Clone + From<f64>,
480{
481    let mut fun_f64 = move |x: &ArrayView1<f64>| fun(x).into();
482
483    let config = TrustRegionConfig {
484        initial_radius: options.trust_radius.unwrap_or(1.0),
485        max_radius: options.max_trust_radius.unwrap_or(100.0),
486        max_iter: options.max_iter,
487        tolerance: options.gtol,
488        ftol: options.ftol,
489        eps: options.eps,
490        min_radius: options.min_trust_radius.unwrap_or(1e-14),
491        ..Default::default()
492    };
493
494    let result = trust_region::trust_region_minimize(
495        &mut fun_f64,
496        None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
497        None::<fn(&ArrayView1<f64>) -> Array2<f64>>,
498        x0,
499        Some(config),
500    )?;
501
502    Ok(OptimizeResult {
503        x: result.x,
504        fun: S::from(result.f_val),
505        nit: result.n_iter,
506        func_evals: result.n_fev,
507        nfev: result.n_fev,
508        jacobian: None,
509        hessian: None,
510        success: result.converged,
511        message: result.message,
512    })
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use approx::assert_abs_diff_eq;
519
520    #[test]
521    fn test_simple_quadratic() {
522        let quadratic = |x: &ArrayView1<f64>| -> f64 { x[0] * x[0] + x[1] * x[1] };
523
524        let x0 = vec![1.0, 1.0];
525        let result = minimize(quadratic, &x0, Method::BFGS, None);
526        assert!(result.is_ok());
527
528        let result = result.expect("Operation failed");
529        assert!(result.success);
530        assert_abs_diff_eq!(result.x[0], 0.0, epsilon = 1e-6);
531        assert_abs_diff_eq!(result.x[1], 0.0, epsilon = 1e-6);
532    }
533}