A simple optimization problem#

Here we provide a compact example on how csnlp can be employed to build and solve an optimization problem. Similar to casadi.Opti, we instantiate a class which represents the NLP and allows us to create its variables and parameters and model its constraints and objective. For example, suppose we’d like to solve the following problem

\[\begin{split}\begin{aligned} \min_{x,y} \quad & (1 - x)^2 + 0.2(y - x^2)^2 \\ \text{s.t.} \quad & \left(\frac{p}{2}\right)^2 \le (x + 0.5)^2 + y^2 \le p^2. \end{aligned}\end{split}\]

We can do so with the following code:

from csnlp import Nlp

nlp = Nlp()
x = nlp.variable("x")[0]  # create primal variable x
y = nlp.variable("y")[0]  # create primal variable y
p = nlp.parameter("p")  # create parameter p

# define the objective and constraints
nlp.minimize((1 - x) ** 2 + 0.2 * (y - x**2) ** 2)
g = (x + 0.5) ** 2 + y**2
nlp.constraint("c1", (p / 2) ** 2, "<=", g)
nlp.constraint("c2", g, "<=", p**2)

nlp.init_solver()  # initializes IPOPT under the hood
sol = nlp.solve(pars={"p": 1.25})  # solves the NLP for parameter p=1.25

x_opt = sol.vals["x"]   # optimal values can be retrieved via the dict .vals
y_opt = sol.value(y)  # or the .value method

Enhancing the NLP class#

However, the package also allows to seamlessly enhance the standard csnlp.Nlp with different capabilities. For instance, when the problem is highly nonlinear and necessitates to be solved with multiple initial conditions, the csnlp.multistart module offers various solutions to parallelize the computations (see, e.g., csnlp.multistart.ParallelMultistartNlp). The csnlp.wrappers module offers instead a set of wrappers that can be used to augment the NLP with additional capabilities, without modifying the original NLP instance: as of now, wrappers have been implemented for

For example, if we’d like to compute the sensitivity \(\frac{\partial y}{\partial p}\) of the optimal primal variable \(y\) with respect to the parameter \(p\), we just need to wrap the csnlp.Nlp instance with the csnlp.wrappers.NlpSensitivity wrapper, which is specialized in differentiating the optimization problem. This in turn allows us to compute the first-order \(\frac{\partial y}{\partial p}\) and second sensitivities \(\frac{\partial^2 y}{\partial p^2}\) (dydp and d2ydp2, respectively) as such:

from csnlp import wrappers

nlp = wrappers.NlpSensitivity(nlp)
dydp, d2ydp2 = nlp.parametric_sensitivity()

In other words, these sensitivities provide the jacobian and hessian that locally approximate the solution w.r.t. the parameter \(p\). As shown in the corresponding example but not in this quick demonstation, the sensitivity can be also computed for any generic expression \(z(x(p),\lambda(p),p)\) that is a function of the primal \(x\) and dual \(\lambda\) variables, and the parameters \(p\). Moreover, the sensitivity computations can be carried out symbolically (more demanding) or numerically (more stable and reliable).

Similarly, a csnlp.Nlp can be wrapped in a csnlp.wrappers.Mpc wrapper that makes it easier to build such finite-horizon optimal controllers for model-based control applications.