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 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
52pub use result::OptimizeResult;
54
55pub 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#[derive(Debug, Clone, Copy)]
118pub enum Method {
119 NelderMead,
121 Powell,
123 CG,
125 BFGS,
127 SR1,
129 DFP,
131 LBFGS,
133 LBFGSB,
135 NewtonCG,
137 TrustNCG,
139 TrustKrylov,
141 TrustExact,
143 TruncatedNewton,
145 TrustRegionNewton,
147 TrustRegionDogleg,
149}
150
151#[derive(Debug, Clone)]
153pub struct Bounds {
154 pub lower: Vec<Option<f64>>,
156 pub upper: Vec<Option<f64>>,
158}
159
160#[derive(Debug, Clone)]
162pub struct Options {
163 pub max_iter: usize,
165 pub max_fev: Option<usize>,
167 pub ftol: f64,
169 pub xtol: f64,
171 pub gtol: f64,
173 pub initial_step: Option<f64>,
175 pub maxstep: Option<f64>,
177 pub finite_diff: bool,
179 pub eps: f64,
181 pub trust_radius: Option<f64>,
183 pub max_trust_radius: Option<f64>,
185 pub min_trust_radius: Option<f64>,
187 pub trust_tol: Option<f64>,
189 pub trust_max_iter: Option<usize>,
191 pub trust_eta: Option<f64>,
193 pub bounds: Option<Bounds>,
195}
196
197impl 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
220impl 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
244impl Bounds {
246 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 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 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 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 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#[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 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#[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 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#[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 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#[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}