ipyopt - Interior Point Optimizing with Python

Gerhard Bräunlich

11 August 2022

🐍 interface to IPOpt

\[ \begin{aligned} \mathrm{min}_{\boldsymbol{x} \in \mathbb{R}^n} f(\boldsymbol{x}) \\ \boldsymbol{g}_l \leq \boldsymbol{g}(\boldsymbol{x}) \leq \boldsymbol{g}_u \in \mathbb{R}^m \\ \boldsymbol{x}_l \leq \boldsymbol{x} \leq \boldsymbol{x}_u \in \mathbb{R}^n \end{aligned} \]

History

2016-10 - 2018-12: ZHAW, WG for renewable energies

  • Develop Model Predictive Controller for a heating system using Optimal Control Theory

  • \(\boldsymbol{x}\) contains timeseries for \(T\), \(P\)
  • \(\boldsymbol{g}(\boldsymbol{x})\) encapsulates constraint that \(T\) solves ODE modelling the building
  • \(f(\boldsymbol{x})\) is e.g. fuel consumption (want to minimize)

=> High dimensional problem / sparse Jacobians

Choosen solutions:

  • IPOpt (C++)
  • Python as glue language
  • pyipopt as interface

Challange: Missing features / bugs in pyipopt interface (e.g. warm start, memory leaks, packaging)

=> PR to github

Repo unmaintained: last reaction 2 years ago 😱

=> Take over maintainership, new project name: ipyopt

How it works

How it works

Improvements

  • 🧹 Code cleanup
  • 📦 Packaging (including 🐧 wheel on pypi)
  • 📖 Docs / Tests / CI
  • 🗜 Compactify python interface
  • ✨ Switch to C++
  • 🚀 Support for scipy.LowLevelCallable / PyCapsules for f, g / Cython modules and derivatives
  • 🤓 Example for symbolic auto differentiation using sympy

What are PyCapsules?

What are PyCapsules?

Auto differentiation

import numpy
import ipyopt
from ipyopt.sym_compile import array_sym, SymNlp

n = 4
m = 2

x = array_sym("x", n)

f = x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2]
g = [x[0] * x[1] * x[2] * x[3], x[0]**2 + x[1]**2 + x[2]**2 + x[3]**2]

c_api = SymNlp(f, g).compile()

nlp = ipyopt.Problem(n=n, x_l=numpy.ones(n), x_u=numpy.full(n, 5.0),
    m=m,
    g_l=numpy.array([25.0, 40.0]), g_u=numpy.array([2.0e19, 40.0]),
    **c_api,
)

SymNlp.compile does:

  • Symbolic derivatives for f, g
  • Generate code for sympy expressions for f, g and derivatives
  • Compile code into a 🐍 C extension
  • import C extension (exposing a dict)

Why not pybind11?

# Wrong type for 1st argument (should be an `int`):
nlp = ipyopt.Problem(float(nvar), x_L, x_U, ...)

Custom C API version:

TypeError: 'float' object cannot be interpreted as an integer

Automatically generated pybind11 error message:

TypeError: __init__(): incompatible constructor arguments. The following argument types are supported:
    1. ipyopt.Problem(n: int, xL: numpy.ndarray[numpy.float64], xU: numpy.ndarray[numpy.float64], m: int, gL: numpy.ndarray[numpy.float64], gU: numpy.ndarray[numpy.float64], sparsity_indices_jac_g: Tuple[List[float], List[float]], sparsity_indices_hess: Tuple[List[float], List[float]], eval_f: function, eval_grad_f: function, eval_g: function, eval_jac_g: function, eval_h: Optional[function] = None, intermediate_callback: Optional[function] = None, x_scaling: Optional[numpy.ndarray[numpy.float64]] = None, g_scaling: Optional[numpy.ndarray[numpy.float64]] = None, obj_scaling: float = 1.0, ipopt_options: Dict[str, Union[int, float, str]] = {})

Invoked with: 4.0, array([1., 1., 1., 1.]), array([5., 5., 5., 5.]), 2, array([25., 40.]), array([2.e+19, 4.e+01]), (array([0, 0, 0, 0, 1, 1, 1, 1]), array([0, 1, 2, 3, 0, 1, 2, 3])), (array([0, 1, 1, 2, 2, 2, 3, 3, 3, 3]), array([0, 0, 1, 0, 1, 2, 0, 1, 2, 3])), <function eval_f at 0x7f3074effd90>, <function eval_grad_f at 0x7f307331b7f0>, <function eval_g at 0x7f307331b880>, <function eval_jac_g at 0x7f307331b910>

Similar projects

  • cyipopt: cython based, top priority: 100% compatibility with scipy.optimize
  • Gurobi: Commercial, but available on Euler

Future

  • Migrate to most recent IPOpt
  • Help wanted: 🪟, 🍏 wheels (challenge: also have to build IPOpt on the target platform)