stevengj / nlopt

library for nonlinear optimization, wrapping many algorithms for global and local, constrained or unconstrained, optimization
Other
1.88k stars 584 forks source link

Support generic (double-sided) inequality constraints #540

Open charelstoncrabb opened 10 months ago

charelstoncrabb commented 10 months ago

The current interface requires (according to the documentation):

a nonlinear inequality constraint of the form $f_c(x) \leq 0$

A very common pattern in constrained optimization is the more general setup of $l_c \leq f_c(x) \leq u_c$. Providing this type of interface seems much more aligned with what a user actually wants to do, and would take a lot of burden off the user to specialize implementations for each type of (upper/lower) constraint.

If I have an existing constraint function $f_c$ (as a part of some external shared runtime e.g.), this requirement significantly complicates wiring into nlopt.

It seems like the logic could be wrapped in an interface something like:

nlopt::opt::add_inequality_constraint(nlopt::func fc, void* userdata, double lc, double uc, double tol)

Somewhat related to why it makes it an unnecessary pain for the user is the lack of support for lambdas (though a separate issue itself).

If lambdas were supported, this would be much simpler, e.g.

opt.add_inequality_constraint(
    [&](unsigned n, const double*x, double*grad, void* data){
        return fc(n,x,grad,data) - ub;
    },
    1.e-8
)
opt.add_inequality_constraint(
    [&](unsigned n, const double*x, double*grad, void* data){
        double ans = lb - fc(n,x,grad,data);
        if (grad) for (unsigned i = 0; i < n; i++) grad[i]*=-1.;
        return ans;
    },
    1.e-8
)

Either the overloaded add_inequality_constraint method above, or overloaded methods

nlopt::opt::add_<objective/constraint/etc> ( std::function<double(unsigned, const double*, double*, void*)> nlopt_fun, ... );

would be fantastic 😁

stevengj commented 2 months ago

This is a little more painful to add than you might expect.

Of course, we could add "double-sided" API, but under the hood it would need to transform it to the $f_c(x) ≤ 0$ form since that's what the algorithms all use. In principle, this is no problem — you create two inequality constraints internally. However, that is suboptimal because it will end up evaluating your $f_c$ function twice. Whereas if the user implements it as two constraints then they can memo-ize the $f_c$ function so that the second call just re-uses the results of the first.