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