1use numra_core::Scalar;
8
9use crate::augmented_lagrangian::AugLagOptions;
10use crate::error::OptimError;
11use crate::global::DEOptions;
12use crate::types::{OptimOptions, OptimResult};
13
14pub type ScalarFn<S> = Box<dyn Fn(&[S]) -> S>;
16pub type VectorFn<S> = Box<dyn Fn(&[S], &mut [S])>;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum VarType {
22 Continuous,
23 Integer,
24 Binary,
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34pub enum ProblemHint {
35 #[default]
37 None,
38 Linear,
40 Quadratic,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum ConstraintKind {
47 Equality,
48 Inequality,
49}
50
51pub struct Constraint<S: Scalar> {
53 pub(crate) func: ScalarFn<S>,
54 pub(crate) grad: Option<VectorFn<S>>,
55 pub(crate) kind: ConstraintKind,
56}
57
58#[derive(Clone, Debug)]
60pub struct LinearConstraint<S: Scalar> {
61 pub(crate) a: Vec<S>,
62 pub(crate) b: S,
63 pub(crate) kind: ConstraintKind,
64}
65
66pub enum ObjectiveKind<S: Scalar> {
68 Linear { c: Vec<S> },
70 Quadratic {
73 h_row_major: Vec<S>,
74 c: Vec<S>,
75 n: usize,
76 },
77 Minimize {
78 func: ScalarFn<S>,
79 grad: Option<VectorFn<S>>,
80 },
81 LeastSquares {
82 residual: VectorFn<S>,
83 jacobian: Option<VectorFn<S>>,
84 n_residuals: usize,
85 },
86 MultiObjective { funcs: Vec<ScalarFn<S>> },
88}
89
90pub struct OptimProblem<S: Scalar> {
95 pub(crate) n: usize,
96 pub(crate) x0: Option<Vec<S>>,
97 pub(crate) bounds: Vec<Option<(S, S)>>,
98 pub(crate) var_types: Vec<VarType>,
99 pub(crate) objective: Option<ObjectiveKind<S>>,
100 pub(crate) constraints: Vec<Constraint<S>>,
101 pub(crate) linear_constraints: Vec<LinearConstraint<S>>,
102 pub(crate) options: OptimOptions<S>,
103 pub(crate) aug_lag_options: Option<AugLagOptions<S>>,
104 pub(crate) global: bool,
105 pub(crate) de_options: Option<DEOptions<S>>,
106 pub(crate) negate_result: bool,
107 pub(crate) hint: ProblemHint,
108}
109
110impl<S: Scalar> OptimProblem<S> {
111 pub fn new(n: usize) -> Self {
113 Self {
114 n,
115 x0: None,
116 bounds: vec![None; n],
117 var_types: vec![VarType::Continuous; n],
118 objective: None,
119 constraints: Vec::new(),
120 linear_constraints: Vec::new(),
121 options: OptimOptions::default(),
122 aug_lag_options: None,
123 global: false,
124 de_options: None,
125 negate_result: false,
126 hint: ProblemHint::None,
127 }
128 }
129
130 pub fn x0(mut self, x0: &[S]) -> Self {
132 self.x0 = Some(x0.to_vec());
133 self
134 }
135
136 pub fn bounds(mut self, i: usize, lo_hi: (S, S)) -> Self {
138 self.bounds[i] = Some(lo_hi);
139 self
140 }
141
142 pub fn all_bounds(mut self, bounds: &[(S, S)]) -> Self {
144 for (i, &b) in bounds.iter().enumerate() {
145 self.bounds[i] = Some(b);
146 }
147 self
148 }
149
150 pub fn integer_var(mut self, i: usize) -> Self {
152 self.var_types[i] = VarType::Integer;
153 self
154 }
155
156 pub fn binary_var(mut self, i: usize) -> Self {
158 self.var_types[i] = VarType::Binary;
159 self.bounds[i] = Some((S::ZERO, S::ONE));
160 self
161 }
162
163 pub fn objective<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
165 match &mut self.objective {
166 Some(ObjectiveKind::Minimize { func, .. }) => {
167 *func = Box::new(f);
168 }
169 _ => {
170 self.objective = Some(ObjectiveKind::Minimize {
171 func: Box::new(f),
172 grad: None,
173 });
174 }
175 }
176 self
177 }
178
179 pub fn gradient<G: Fn(&[S], &mut [S]) + 'static>(mut self, g: G) -> Self {
181 match &mut self.objective {
182 Some(ObjectiveKind::Minimize { grad, .. }) => {
183 *grad = Some(Box::new(g));
184 }
185 _ => {
186 self.objective = Some(ObjectiveKind::Minimize {
187 func: Box::new(|_| S::ZERO),
188 grad: Some(Box::new(g)),
189 });
190 }
191 }
192 self
193 }
194
195 pub fn maximize<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
199 self.objective = Some(ObjectiveKind::Minimize {
200 func: Box::new(move |x: &[S]| -f(x)),
201 grad: None,
202 });
203 self.negate_result = true;
204 self
205 }
206
207 pub fn maximize_gradient<G: Fn(&[S], &mut [S]) + 'static>(mut self, g: G) -> Self {
212 if let Some(ObjectiveKind::Minimize { grad, .. }) = &mut self.objective {
213 if self.negate_result {
214 *grad = Some(Box::new(move |x: &[S], gout: &mut [S]| {
215 g(x, gout);
216 for gi in gout.iter_mut() {
217 *gi = -*gi;
218 }
219 }));
220 }
221 }
222 self
223 }
224
225 pub fn least_squares<R: Fn(&[S], &mut [S]) + 'static>(
227 mut self,
228 n_residuals: usize,
229 residual: R,
230 ) -> Self {
231 self.objective = Some(ObjectiveKind::LeastSquares {
232 residual: Box::new(residual),
233 jacobian: None,
234 n_residuals,
235 });
236 self
237 }
238
239 pub fn jacobian<J: Fn(&[S], &mut [S]) + 'static>(mut self, j: J) -> Self {
241 if let Some(ObjectiveKind::LeastSquares { jacobian, .. }) = &mut self.objective {
242 *jacobian = Some(Box::new(j));
243 }
244 self
245 }
246
247 pub fn linear_objective(mut self, c: &[S]) -> Self {
249 assert_eq!(c.len(), self.n, "linear objective dimension mismatch");
250 self.objective = Some(ObjectiveKind::Linear { c: c.to_vec() });
251 self
252 }
253
254 pub fn quadratic_objective_dense(mut self, h_row_major: &[S], c: &[S]) -> Self {
257 let n = self.n;
258 assert_eq!(h_row_major.len(), n * n, "Hessian dimension mismatch");
259 assert_eq!(c.len(), n, "linear term dimension mismatch");
260 self.objective = Some(ObjectiveKind::Quadratic {
261 h_row_major: h_row_major.to_vec(),
262 c: c.to_vec(),
263 n,
264 });
265 self
266 }
267
268 pub fn linear_constraint_ineq(mut self, a: &[S], b: S) -> Self {
270 assert_eq!(a.len(), self.n, "constraint dimension mismatch");
271 self.linear_constraints.push(LinearConstraint {
272 a: a.to_vec(),
273 b,
274 kind: ConstraintKind::Inequality,
275 });
276 self
277 }
278
279 pub fn linear_constraint_eq(mut self, a: &[S], b: S) -> Self {
281 assert_eq!(a.len(), self.n, "constraint dimension mismatch");
282 self.linear_constraints.push(LinearConstraint {
283 a: a.to_vec(),
284 b,
285 kind: ConstraintKind::Equality,
286 });
287 self
288 }
289
290 pub fn constraint_ineq<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
292 self.constraints.push(Constraint {
293 func: Box::new(f),
294 grad: None,
295 kind: ConstraintKind::Inequality,
296 });
297 self
298 }
299
300 pub fn constraint_eq<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
302 self.constraints.push(Constraint {
303 func: Box::new(f),
304 grad: None,
305 kind: ConstraintKind::Equality,
306 });
307 self
308 }
309
310 pub fn constraint_ineq_with_grad<F, G>(mut self, f: F, g: G) -> Self
312 where
313 F: Fn(&[S]) -> S + 'static,
314 G: Fn(&[S], &mut [S]) + 'static,
315 {
316 self.constraints.push(Constraint {
317 func: Box::new(f),
318 grad: Some(Box::new(g)),
319 kind: ConstraintKind::Inequality,
320 });
321 self
322 }
323
324 pub fn constraint_eq_with_grad<F, G>(mut self, f: F, g: G) -> Self
326 where
327 F: Fn(&[S]) -> S + 'static,
328 G: Fn(&[S], &mut [S]) + 'static,
329 {
330 self.constraints.push(Constraint {
331 func: Box::new(f),
332 grad: Some(Box::new(g)),
333 kind: ConstraintKind::Equality,
334 });
335 self
336 }
337
338 pub fn max_iter(mut self, n: usize) -> Self {
340 self.options.max_iter = n;
341 self
342 }
343
344 pub fn gtol(mut self, tol: S) -> Self {
346 self.options.gtol = tol;
347 self
348 }
349
350 pub fn options(mut self, opts: OptimOptions<S>) -> Self {
352 self.options = opts;
353 self
354 }
355
356 pub fn aug_lag_options(mut self, opts: AugLagOptions<S>) -> Self {
358 self.aug_lag_options = Some(opts);
359 self
360 }
361
362 pub fn ctol(mut self, tol: S) -> Self {
364 let opts = self
365 .aug_lag_options
366 .get_or_insert_with(AugLagOptions::default);
367 opts.ctol = tol;
368 self
369 }
370
371 pub fn global(mut self, enabled: bool) -> Self {
373 self.global = enabled;
374 self
375 }
376
377 pub fn de_options(mut self, opts: DEOptions<S>) -> Self {
379 self.de_options = Some(opts);
380 self
381 }
382
383 pub fn multi_objective(mut self, funcs: Vec<ScalarFn<S>>) -> Self {
385 self.objective = Some(ObjectiveKind::MultiObjective { funcs });
386 self
387 }
388
389 pub fn hint(mut self, hint: ProblemHint) -> Self {
395 self.hint = hint;
396 self
397 }
398
399 pub fn has_bounds(&self) -> bool {
403 self.bounds.iter().any(|b| b.is_some())
404 }
405
406 pub fn has_constraints(&self) -> bool {
408 !self.constraints.is_empty()
409 }
410
411 pub fn is_least_squares(&self) -> bool {
413 matches!(self.objective, Some(ObjectiveKind::LeastSquares { .. }))
414 }
415
416 pub fn is_linear(&self) -> bool {
418 matches!(self.objective, Some(ObjectiveKind::Linear { .. }))
419 }
420
421 pub fn is_quadratic(&self) -> bool {
423 matches!(self.objective, Some(ObjectiveKind::Quadratic { .. }))
424 }
425
426 pub fn is_hint_linear(&self) -> bool {
428 self.hint == ProblemHint::Linear
429 }
430
431 pub fn is_hint_quadratic(&self) -> bool {
433 self.hint == ProblemHint::Quadratic
434 }
435
436 pub fn is_multi_objective(&self) -> bool {
438 matches!(self.objective, Some(ObjectiveKind::MultiObjective { .. }))
439 }
440
441 pub fn has_linear_constraints(&self) -> bool {
443 !self.linear_constraints.is_empty()
444 }
445
446 pub fn has_integer_vars(&self) -> bool {
448 self.var_types.iter().any(|v| *v != VarType::Continuous)
449 }
450
451 pub fn n_eq_constraints(&self) -> usize {
453 self.constraints
454 .iter()
455 .filter(|c| c.kind == ConstraintKind::Equality)
456 .count()
457 }
458
459 pub fn n_ineq_constraints(&self) -> usize {
461 self.constraints
462 .iter()
463 .filter(|c| c.kind == ConstraintKind::Inequality)
464 .count()
465 }
466}
467
468impl<S: Scalar + faer::SimpleEntity + faer::Conjugate<Canonical = S> + faer::ComplexField>
469 OptimProblem<S>
470{
471 pub fn solve(self) -> Result<OptimResult<S>, OptimError> {
473 crate::auto::auto_minimize(self)
474 }
475
476 pub fn solve_with(
478 self,
479 choice: crate::auto::SolverChoice,
480 ) -> Result<OptimResult<S>, OptimError> {
481 crate::auto::dispatch(self, choice)
482 }
483}
484
485pub fn finite_diff_gradient<S: Scalar>(f: &dyn Fn(&[S]) -> S, x: &[S], g: &mut [S]) {
491 let n = x.len();
492 let h_factor = S::EPSILON.cbrt();
493 let mut xp = x.to_vec();
494 for i in 0..n {
495 let xi_orig = xp[i];
496 let h = h_factor * (S::ONE + xi_orig.abs());
497 xp[i] = xi_orig + h;
498 let fp = f(&xp);
499 xp[i] = xi_orig - h;
500 let fm = f(&xp);
501 g[i] = (fp - fm) / (S::TWO * h);
502 xp[i] = xi_orig;
503 }
504}
505
506pub fn finite_diff_jacobian<S: Scalar>(
515 r: &dyn Fn(&[S], &mut [S]),
516 x: &[S],
517 m: usize,
518 jac: &mut [S],
519) {
520 let n = x.len();
521 let h_factor = S::EPSILON.cbrt();
522 let mut xp = x.to_vec();
523 let mut rp = vec![S::ZERO; m];
524 let mut rm = vec![S::ZERO; m];
525 for j in 0..n {
526 let xj_orig = xp[j];
527 let h = h_factor * (S::ONE + xj_orig.abs());
528 xp[j] = xj_orig + h;
529 r(&xp, &mut rp);
530 xp[j] = xj_orig - h;
531 r(&xp, &mut rm);
532 for i in 0..m {
533 jac[i * n + j] = (rp[i] - rm[i]) / (S::TWO * h);
534 }
535 xp[j] = xj_orig;
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_builder_construction() {
545 let problem = OptimProblem::<f64>::new(2)
546 .x0(&[1.0, 2.0])
547 .objective(|x: &[f64]| x[0] * x[0] + x[1] * x[1])
548 .gradient(|x: &[f64], g: &mut [f64]| {
549 g[0] = 2.0 * x[0];
550 g[1] = 2.0 * x[1];
551 })
552 .gtol(1e-10);
553
554 assert!(!problem.has_bounds());
555 assert!(!problem.has_constraints());
556 assert!(!problem.is_least_squares());
557 assert_eq!(problem.n_eq_constraints(), 0);
558 assert_eq!(problem.n_ineq_constraints(), 0);
559 }
560
561 #[test]
562 fn test_builder_with_bounds() {
563 let problem = OptimProblem::<f64>::new(3)
564 .x0(&[0.0, 0.0, 0.0])
565 .objective(|x: &[f64]| x.iter().copied().map(|xi| xi * xi).sum::<f64>())
566 .bounds(0, (-1.0, 1.0))
567 .bounds(2, (0.0, 10.0));
568
569 assert!(problem.has_bounds());
570 assert!(problem.bounds[0].is_some());
571 assert!(problem.bounds[1].is_none());
572 assert!(problem.bounds[2].is_some());
573 }
574
575 #[test]
576 fn test_builder_with_constraints() {
577 let problem = OptimProblem::<f64>::new(2)
578 .x0(&[1.0, 1.0])
579 .objective(|x: &[f64]| x[0] + x[1])
580 .constraint_eq(|x: &[f64]| x[0] * x[0] + x[1] * x[1] - 1.0)
581 .constraint_ineq(|x: &[f64]| x[0] - 0.5);
582
583 assert!(problem.has_constraints());
584 assert_eq!(problem.n_eq_constraints(), 1);
585 assert_eq!(problem.n_ineq_constraints(), 1);
586 }
587
588 #[test]
589 fn test_builder_least_squares() {
590 let problem = OptimProblem::<f64>::new(2).x0(&[0.0, 0.0]).least_squares(
591 3,
592 |x: &[f64], r: &mut [f64]| {
593 r[0] = x[0] - 1.0;
594 r[1] = x[1] - 2.0;
595 r[2] = x[0] + x[1] - 3.0;
596 },
597 );
598
599 assert!(problem.is_least_squares());
600 }
601
602 #[test]
603 fn test_builder_linear_objective() {
604 let p = OptimProblem::<f64>::new(3).linear_objective(&[2.0, 3.0, 1.0]);
605 assert!(p.is_linear());
606 assert!(!p.is_quadratic());
607 assert!(!p.is_least_squares());
608 }
609
610 #[test]
611 fn test_builder_quadratic_objective() {
612 let p = OptimProblem::<f64>::new(2)
613 .quadratic_objective_dense(&[2.0, 0.0, 0.0, 4.0], &[1.0, 1.0]);
614 assert!(p.is_quadratic());
615 assert!(!p.is_linear());
616 }
617
618 #[test]
619 fn test_builder_linear_constraints() {
620 let p = OptimProblem::<f64>::new(2)
621 .linear_objective(&[1.0, 2.0])
622 .linear_constraint_ineq(&[1.0, 1.0], 10.0)
623 .linear_constraint_eq(&[1.0, -1.0], 0.0);
624 assert!(p.has_linear_constraints());
625 assert_eq!(p.linear_constraints.len(), 2);
626 }
627
628 #[test]
629 fn test_builder_integer_vars() {
630 let p = OptimProblem::<f64>::new(3)
631 .linear_objective(&[1.0, 2.0, 3.0])
632 .integer_var(0)
633 .binary_var(2);
634 assert!(p.has_integer_vars());
635 assert_eq!(p.var_types[0], VarType::Integer);
636 assert_eq!(p.var_types[1], VarType::Continuous);
637 assert_eq!(p.var_types[2], VarType::Binary);
638 assert_eq!(p.bounds[2], Some((0.0, 1.0)));
639 }
640
641 #[test]
642 fn test_finite_diff_gradient() {
643 let f = |x: &[f64]| x[0] * x[0] + 4.0 * x[1] * x[1];
644 let x = [3.0, 2.0];
645 let mut g = [0.0; 2];
646 finite_diff_gradient(&f, &x, &mut g);
647 assert!((g[0] - 6.0).abs() < 1e-5);
649 assert!((g[1] - 16.0).abs() < 1e-5);
650 }
651
652 #[test]
653 fn test_finite_diff_gradient_large_x_no_scaling_bug() {
654 let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
659 let x = [1e8, 1e8];
660 let mut g = [0.0; 2];
661 finite_diff_gradient(&f, &x, &mut g);
662 let expected = 2e8;
664 assert!(
665 (g[0] - expected).abs() < 1e-3 * expected.abs(),
666 "g[0] = {} should be ≈ {} (within 1e-3 relative); old unscaled formula returns 0",
667 g[0],
668 expected
669 );
670 assert!(
671 (g[1] - expected).abs() < 1e-3 * expected.abs(),
672 "g[1] = {} should be ≈ {} (within 1e-3 relative); old unscaled formula returns 0",
673 g[1],
674 expected
675 );
676 }
677
678 #[test]
679 fn test_finite_diff_jacobian_large_x_no_scaling_bug() {
680 let r = |x: &[f64], out: &mut [f64]| {
683 out[0] = x[0] * x[0];
684 out[1] = x[1] * x[1];
685 };
686 let x = [1e8, 1e8];
687 let mut jac = [0.0; 4];
688 finite_diff_jacobian(&r, &x, 2, &mut jac);
689 let expected = 2e8;
691 assert!(
692 (jac[0] - expected).abs() < 1e-3 * expected.abs(),
693 "J[0,0] = {} should be ≈ {} (within 1e-3 relative); old unscaled formula returns 0",
694 jac[0],
695 expected
696 );
697 assert!(
698 (jac[3] - expected).abs() < 1e-3 * expected.abs(),
699 "J[1,1] = {} should be ≈ {} (within 1e-3 relative); old unscaled formula returns 0",
700 jac[3],
701 expected
702 );
703 assert!(jac[1].abs() < 1e-3 * expected.abs());
705 assert!(jac[2].abs() < 1e-3 * expected.abs());
706 }
707}