Constraints, gradients and global optimisation

SciPy · Advanced · 14 min

Real problems have limits (budgets, physical bounds) and many local minima that basic minimize misses.

Bounds restrict each variable: minimize(f, x0, bounds=[(0, None), (-1, 1)], method="L-BFGS-B"). None means unbounded on that side.

Constraints go in a list: {"type": "ineq", "fun": g} means g(x) >= 0, "eq" means g(x) = 0, or use NonlinearConstraint(g, lb, ub) and LinearConstraint(A, lb, ub). Methods that handle general constraints are "SLSQP", "trust-constr" and "COBYQA".

Gradients make optimisers faster and more reliable. Pass jac=grad_f, or jac=True when fun returns (value, gradient). Without them SciPy uses finite differences, which costs extra evaluations per step and loses accuracy. Check hand-written gradients with optimize.check_grad.

Linear and integer problems have dedicated solvers: optimize.linprog (HiGHS) for linear programs and optimize.milp for mixed-integer linear programs. They are far more efficient than generic minimize for such problems.

Global optimisation for multimodal functions within bounds:

  • differential_evolution: population-based, robust, parallelisable via workers,
  • dual_annealing, basinhopping, shgo, direct.

They cost many more evaluations, so they suit cheap functions with few variables. A common pattern is a global search followed by a local minimize polish, which differential_evolution does by default (polish=True).

Example

import numpy as np
from scipy import optimize

# maximise x*y subject to x + 2y <= 10, x, y >= 0  (minimise -x*y)
cons = [{"type": "ineq", "fun": lambda v: 10 - v[0] - 2 * v[1]}]
res = optimize.minimize(lambda v: -v[0] * v[1], x0=[1, 1],
                        bounds=[(0, None), (0, None)], constraints=cons, method="SLSQP")
print(res.x.round(3), round(-res.fun, 3))      # [5.  2.5] 12.5

def rastrigin(x):                              # many local minima, global min at 0
    return 10 * len(x) + np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x))

local = optimize.minimize(rastrigin, x0=[3.2, -2.8])
glob = optimize.differential_evolution(rastrigin, bounds=[(-5, 5)] * 2, rng=0)
print(round(local.fun, 2), round(glob.fun, 6))  # 17.91 0.0

lp = optimize.linprog(c=[-3, -2], A_ub=[[1, 1], [1, 3]], b_ub=[4, 6], bounds=[(0, None)] * 2)
print(lp.x, -lp.fun)                            # [4. 0.] 12.0

Remember

  • Bounds need a method like L-BFGS-B; general constraints need SLSQP, trust-constr or COBYQA
  • Supply jac for speed and accuracy
  • Use linprog/milp for linear problems and global optimisers for multimodal ones

Watch out for

  • Using a local optimiser on a function with many minima and trusting the first answer.

Check yourself

  1. In SciPy's dict-style constraints, what does {"type": "ineq", "fun": g} require?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scipy.org/doc/scipy/reference/optimize.html