1use crate::error::OptimizeError;
6use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
7use std::fmt;
8
9pub enum Jacobian<'a> {
11 FiniteDiff,
13 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
26pub 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
54pub use result::OptimizeResult;
56
57pub 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#[derive(Debug, Clone, Copy)]
120pub enum Method {
121 NelderMead,
123 Powell,
125 CG,
127 BFGS,
129 SR1,
131 DFP,
133 LBFGS,
135 LBFGSB,
137 NewtonCG,
139 TrustNCG,
141 TrustKrylov,
143 TrustExact,
145 TruncatedNewton,
147 TrustRegionNewton,
149 TrustRegionDogleg,
151}
152
153#[derive(Debug, Clone)]
155pub struct Bounds {
156 pub lower: Vec<Option<f64>>,
158 pub upper: Vec<Option<f64>>,
160}
161
162#[derive(Debug, Clone)]
164pub struct Options {
165 pub max_iter: usize,
167 pub max_fev: Option<usize>,
169 pub ftol: f64,
171 pub xtol: f64,
173 pub gtol: f64,
175 pub initial_step: Option<f64>,
177 pub maxstep: Option<f64>,
179 pub finite_diff: bool,
181 pub eps: f64,
183 pub trust_radius: Option<f64>,
185 pub max_trust_radius: Option<f64>,
187 pub min_trust_radius: Option<f64>,
189 pub trust_tol: Option<f64>,
191 pub trust_max_iter: Option<usize>,
193 pub trust_eta: Option<f64>,
195 pub bounds: Option<Bounds>,
197}
198
199impl 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
222impl 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
246impl Bounds {
248 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 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 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 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 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#[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 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#[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 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#[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 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#[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}