use numra_core::Scalar;
use crate::augmented_lagrangian::AugLagOptions;
use crate::error::OptimError;
use crate::global::DEOptions;
use crate::types::{OptimOptions, OptimResult};
pub type ScalarFn<S> = Box<dyn Fn(&[S]) -> S>;
pub type VectorFn<S> = Box<dyn Fn(&[S], &mut [S])>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VarType {
Continuous,
Integer,
Binary,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ProblemHint {
#[default]
None,
Linear,
Quadratic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConstraintKind {
Equality,
Inequality,
}
pub struct Constraint<S: Scalar> {
pub(crate) func: ScalarFn<S>,
pub(crate) grad: Option<VectorFn<S>>,
pub(crate) kind: ConstraintKind,
}
#[derive(Clone, Debug)]
pub struct LinearConstraint<S: Scalar> {
pub(crate) a: Vec<S>,
pub(crate) b: S,
pub(crate) kind: ConstraintKind,
}
pub enum ObjectiveKind<S: Scalar> {
Linear { c: Vec<S> },
Quadratic {
h_row_major: Vec<S>,
c: Vec<S>,
n: usize,
},
Minimize {
func: ScalarFn<S>,
grad: Option<VectorFn<S>>,
},
LeastSquares {
residual: VectorFn<S>,
jacobian: Option<VectorFn<S>>,
n_residuals: usize,
},
MultiObjective { funcs: Vec<ScalarFn<S>> },
}
pub struct OptimProblem<S: Scalar> {
pub(crate) n: usize,
pub(crate) x0: Option<Vec<S>>,
pub(crate) bounds: Vec<Option<(S, S)>>,
pub(crate) var_types: Vec<VarType>,
pub(crate) objective: Option<ObjectiveKind<S>>,
pub(crate) constraints: Vec<Constraint<S>>,
pub(crate) linear_constraints: Vec<LinearConstraint<S>>,
pub(crate) options: OptimOptions<S>,
pub(crate) aug_lag_options: Option<AugLagOptions<S>>,
pub(crate) global: bool,
pub(crate) de_options: Option<DEOptions<S>>,
pub(crate) negate_result: bool,
pub(crate) hint: ProblemHint,
}
impl<S: Scalar> OptimProblem<S> {
pub fn new(n: usize) -> Self {
Self {
n,
x0: None,
bounds: vec![None; n],
var_types: vec![VarType::Continuous; n],
objective: None,
constraints: Vec::new(),
linear_constraints: Vec::new(),
options: OptimOptions::default(),
aug_lag_options: None,
global: false,
de_options: None,
negate_result: false,
hint: ProblemHint::None,
}
}
pub fn x0(mut self, x0: &[S]) -> Self {
self.x0 = Some(x0.to_vec());
self
}
pub fn bounds(mut self, i: usize, lo_hi: (S, S)) -> Self {
self.bounds[i] = Some(lo_hi);
self
}
pub fn all_bounds(mut self, bounds: &[(S, S)]) -> Self {
for (i, &b) in bounds.iter().enumerate() {
self.bounds[i] = Some(b);
}
self
}
pub fn integer_var(mut self, i: usize) -> Self {
self.var_types[i] = VarType::Integer;
self
}
pub fn binary_var(mut self, i: usize) -> Self {
self.var_types[i] = VarType::Binary;
self.bounds[i] = Some((S::ZERO, S::ONE));
self
}
pub fn objective<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
match &mut self.objective {
Some(ObjectiveKind::Minimize { func, .. }) => {
*func = Box::new(f);
}
_ => {
self.objective = Some(ObjectiveKind::Minimize {
func: Box::new(f),
grad: None,
});
}
}
self
}
pub fn gradient<G: Fn(&[S], &mut [S]) + 'static>(mut self, g: G) -> Self {
match &mut self.objective {
Some(ObjectiveKind::Minimize { grad, .. }) => {
*grad = Some(Box::new(g));
}
_ => {
self.objective = Some(ObjectiveKind::Minimize {
func: Box::new(|_| S::ZERO),
grad: Some(Box::new(g)),
});
}
}
self
}
pub fn maximize<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
self.objective = Some(ObjectiveKind::Minimize {
func: Box::new(move |x: &[S]| -f(x)),
grad: None,
});
self.negate_result = true;
self
}
pub fn maximize_gradient<G: Fn(&[S], &mut [S]) + 'static>(mut self, g: G) -> Self {
if let Some(ObjectiveKind::Minimize { grad, .. }) = &mut self.objective {
if self.negate_result {
*grad = Some(Box::new(move |x: &[S], gout: &mut [S]| {
g(x, gout);
for gi in gout.iter_mut() {
*gi = -*gi;
}
}));
}
}
self
}
pub fn least_squares<R: Fn(&[S], &mut [S]) + 'static>(
mut self,
n_residuals: usize,
residual: R,
) -> Self {
self.objective = Some(ObjectiveKind::LeastSquares {
residual: Box::new(residual),
jacobian: None,
n_residuals,
});
self
}
pub fn jacobian<J: Fn(&[S], &mut [S]) + 'static>(mut self, j: J) -> Self {
if let Some(ObjectiveKind::LeastSquares { jacobian, .. }) = &mut self.objective {
*jacobian = Some(Box::new(j));
}
self
}
pub fn linear_objective(mut self, c: &[S]) -> Self {
assert_eq!(c.len(), self.n, "linear objective dimension mismatch");
self.objective = Some(ObjectiveKind::Linear { c: c.to_vec() });
self
}
pub fn quadratic_objective_dense(mut self, h_row_major: &[S], c: &[S]) -> Self {
let n = self.n;
assert_eq!(h_row_major.len(), n * n, "Hessian dimension mismatch");
assert_eq!(c.len(), n, "linear term dimension mismatch");
self.objective = Some(ObjectiveKind::Quadratic {
h_row_major: h_row_major.to_vec(),
c: c.to_vec(),
n,
});
self
}
pub fn linear_constraint_ineq(mut self, a: &[S], b: S) -> Self {
assert_eq!(a.len(), self.n, "constraint dimension mismatch");
self.linear_constraints.push(LinearConstraint {
a: a.to_vec(),
b,
kind: ConstraintKind::Inequality,
});
self
}
pub fn linear_constraint_eq(mut self, a: &[S], b: S) -> Self {
assert_eq!(a.len(), self.n, "constraint dimension mismatch");
self.linear_constraints.push(LinearConstraint {
a: a.to_vec(),
b,
kind: ConstraintKind::Equality,
});
self
}
pub fn constraint_ineq<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
self.constraints.push(Constraint {
func: Box::new(f),
grad: None,
kind: ConstraintKind::Inequality,
});
self
}
pub fn constraint_eq<F: Fn(&[S]) -> S + 'static>(mut self, f: F) -> Self {
self.constraints.push(Constraint {
func: Box::new(f),
grad: None,
kind: ConstraintKind::Equality,
});
self
}
pub fn constraint_ineq_with_grad<F, G>(mut self, f: F, g: G) -> Self
where
F: Fn(&[S]) -> S + 'static,
G: Fn(&[S], &mut [S]) + 'static,
{
self.constraints.push(Constraint {
func: Box::new(f),
grad: Some(Box::new(g)),
kind: ConstraintKind::Inequality,
});
self
}
pub fn constraint_eq_with_grad<F, G>(mut self, f: F, g: G) -> Self
where
F: Fn(&[S]) -> S + 'static,
G: Fn(&[S], &mut [S]) + 'static,
{
self.constraints.push(Constraint {
func: Box::new(f),
grad: Some(Box::new(g)),
kind: ConstraintKind::Equality,
});
self
}
pub fn max_iter(mut self, n: usize) -> Self {
self.options.max_iter = n;
self
}
pub fn gtol(mut self, tol: S) -> Self {
self.options.gtol = tol;
self
}
pub fn options(mut self, opts: OptimOptions<S>) -> Self {
self.options = opts;
self
}
pub fn aug_lag_options(mut self, opts: AugLagOptions<S>) -> Self {
self.aug_lag_options = Some(opts);
self
}
pub fn ctol(mut self, tol: S) -> Self {
let opts = self
.aug_lag_options
.get_or_insert_with(AugLagOptions::default);
opts.ctol = tol;
self
}
pub fn global(mut self, enabled: bool) -> Self {
self.global = enabled;
self
}
pub fn de_options(mut self, opts: DEOptions<S>) -> Self {
self.de_options = Some(opts);
self
}
pub fn multi_objective(mut self, funcs: Vec<ScalarFn<S>>) -> Self {
self.objective = Some(ObjectiveKind::MultiObjective { funcs });
self
}
pub fn hint(mut self, hint: ProblemHint) -> Self {
self.hint = hint;
self
}
pub fn has_bounds(&self) -> bool {
self.bounds.iter().any(|b| b.is_some())
}
pub fn has_constraints(&self) -> bool {
!self.constraints.is_empty()
}
pub fn is_least_squares(&self) -> bool {
matches!(self.objective, Some(ObjectiveKind::LeastSquares { .. }))
}
pub fn is_linear(&self) -> bool {
matches!(self.objective, Some(ObjectiveKind::Linear { .. }))
}
pub fn is_quadratic(&self) -> bool {
matches!(self.objective, Some(ObjectiveKind::Quadratic { .. }))
}
pub fn is_hint_linear(&self) -> bool {
self.hint == ProblemHint::Linear
}
pub fn is_hint_quadratic(&self) -> bool {
self.hint == ProblemHint::Quadratic
}
pub fn is_multi_objective(&self) -> bool {
matches!(self.objective, Some(ObjectiveKind::MultiObjective { .. }))
}
pub fn has_linear_constraints(&self) -> bool {
!self.linear_constraints.is_empty()
}
pub fn has_integer_vars(&self) -> bool {
self.var_types.iter().any(|v| *v != VarType::Continuous)
}
pub fn n_eq_constraints(&self) -> usize {
self.constraints
.iter()
.filter(|c| c.kind == ConstraintKind::Equality)
.count()
}
pub fn n_ineq_constraints(&self) -> usize {
self.constraints
.iter()
.filter(|c| c.kind == ConstraintKind::Inequality)
.count()
}
}
impl<S: Scalar + faer::SimpleEntity + faer::Conjugate<Canonical = S> + faer::ComplexField>
OptimProblem<S>
{
pub fn solve(self) -> Result<OptimResult<S>, OptimError> {
crate::auto::auto_minimize(self)
}
pub fn solve_with(
self,
choice: crate::auto::SolverChoice,
) -> Result<OptimResult<S>, OptimError> {
crate::auto::dispatch(self, choice)
}
}
pub fn finite_diff_gradient<S: Scalar>(f: &dyn Fn(&[S]) -> S, x: &[S], g: &mut [S]) {
let n = x.len();
let eps = S::from_f64(1e-8);
let mut xp = x.to_vec();
for i in 0..n {
let xi_orig = xp[i];
xp[i] = xi_orig + eps;
let fp = f(&xp);
xp[i] = xi_orig - eps;
let fm = f(&xp);
g[i] = (fp - fm) / (S::TWO * eps);
xp[i] = xi_orig;
}
}
pub fn finite_diff_jacobian<S: Scalar>(
r: &dyn Fn(&[S], &mut [S]),
x: &[S],
m: usize,
jac: &mut [S],
) {
let n = x.len();
let eps = S::from_f64(1e-8);
let mut xp = x.to_vec();
let mut rp = vec![S::ZERO; m];
let mut rm = vec![S::ZERO; m];
for j in 0..n {
let xj_orig = xp[j];
xp[j] = xj_orig + eps;
r(&xp, &mut rp);
xp[j] = xj_orig - eps;
r(&xp, &mut rm);
for i in 0..m {
jac[i * n + j] = (rp[i] - rm[i]) / (S::TWO * eps);
}
xp[j] = xj_orig;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_construction() {
let problem = OptimProblem::<f64>::new(2)
.x0(&[1.0, 2.0])
.objective(|x: &[f64]| x[0] * x[0] + x[1] * x[1])
.gradient(|x: &[f64], g: &mut [f64]| {
g[0] = 2.0 * x[0];
g[1] = 2.0 * x[1];
})
.gtol(1e-10);
assert!(!problem.has_bounds());
assert!(!problem.has_constraints());
assert!(!problem.is_least_squares());
assert_eq!(problem.n_eq_constraints(), 0);
assert_eq!(problem.n_ineq_constraints(), 0);
}
#[test]
fn test_builder_with_bounds() {
let problem = OptimProblem::<f64>::new(3)
.x0(&[0.0, 0.0, 0.0])
.objective(|x: &[f64]| x.iter().copied().map(|xi| xi * xi).sum::<f64>())
.bounds(0, (-1.0, 1.0))
.bounds(2, (0.0, 10.0));
assert!(problem.has_bounds());
assert!(problem.bounds[0].is_some());
assert!(problem.bounds[1].is_none());
assert!(problem.bounds[2].is_some());
}
#[test]
fn test_builder_with_constraints() {
let problem = OptimProblem::<f64>::new(2)
.x0(&[1.0, 1.0])
.objective(|x: &[f64]| x[0] + x[1])
.constraint_eq(|x: &[f64]| x[0] * x[0] + x[1] * x[1] - 1.0)
.constraint_ineq(|x: &[f64]| x[0] - 0.5);
assert!(problem.has_constraints());
assert_eq!(problem.n_eq_constraints(), 1);
assert_eq!(problem.n_ineq_constraints(), 1);
}
#[test]
fn test_builder_least_squares() {
let problem = OptimProblem::<f64>::new(2).x0(&[0.0, 0.0]).least_squares(
3,
|x: &[f64], r: &mut [f64]| {
r[0] = x[0] - 1.0;
r[1] = x[1] - 2.0;
r[2] = x[0] + x[1] - 3.0;
},
);
assert!(problem.is_least_squares());
}
#[test]
fn test_builder_linear_objective() {
let p = OptimProblem::<f64>::new(3).linear_objective(&[2.0, 3.0, 1.0]);
assert!(p.is_linear());
assert!(!p.is_quadratic());
assert!(!p.is_least_squares());
}
#[test]
fn test_builder_quadratic_objective() {
let p = OptimProblem::<f64>::new(2)
.quadratic_objective_dense(&[2.0, 0.0, 0.0, 4.0], &[1.0, 1.0]);
assert!(p.is_quadratic());
assert!(!p.is_linear());
}
#[test]
fn test_builder_linear_constraints() {
let p = OptimProblem::<f64>::new(2)
.linear_objective(&[1.0, 2.0])
.linear_constraint_ineq(&[1.0, 1.0], 10.0)
.linear_constraint_eq(&[1.0, -1.0], 0.0);
assert!(p.has_linear_constraints());
assert_eq!(p.linear_constraints.len(), 2);
}
#[test]
fn test_builder_integer_vars() {
let p = OptimProblem::<f64>::new(3)
.linear_objective(&[1.0, 2.0, 3.0])
.integer_var(0)
.binary_var(2);
assert!(p.has_integer_vars());
assert_eq!(p.var_types[0], VarType::Integer);
assert_eq!(p.var_types[1], VarType::Continuous);
assert_eq!(p.var_types[2], VarType::Binary);
assert_eq!(p.bounds[2], Some((0.0, 1.0)));
}
#[test]
fn test_finite_diff_gradient() {
let f = |x: &[f64]| x[0] * x[0] + 4.0 * x[1] * x[1];
let x = [3.0, 2.0];
let mut g = [0.0; 2];
finite_diff_gradient(&f, &x, &mut g);
assert!((g[0] - 6.0).abs() < 1e-5);
assert!((g[1] - 16.0).abs() < 1e-5);
}
}