Note
Go to the end to download the full example code.
A simple problem with multiple minima#
In this example, we show how to solve a simple nonlinear optimization problem with
multiple minimas using the multistart capabilities of csnlp.multistart.
The problem we consider is
which, in the given range, has three local minima.
Without csnlp.multistart#
If we attempt in solving the given problem using the standard NLP formulation, i.e., with a single initial condition, we might end up in a local minimum. The chances of this occurring increase with the complexity and dimension of the problem.
First, we’ll solve the problem the usual way with csnlp.Nlp. Let’s define
the imports and the function to optimize.
Then, let’s build the NLP.
LB, UB = -0.5, 1.4
nlp = Nlp[cs.SX]()
x = nlp.variable("x", lb=LB, ub=UB)[0]
p0 = nlp.parameter("p0")
p1 = nlp.parameter("p1")
nlp.minimize(func(x, p0, p1))
opts = {"print_time": False, "ipopt": {"sb": "yes", "print_level": 0}}
nlp.init_solver(opts)
We can solve it with some (single) initial conditions, e.g., \(x_0 = 0.0\). We fix the parameters to some constant values throughout the example.
If we plot this solution, we’ll see that the gradient-based solver IPOPT got stuck in a local minimum.
fig, ax = plt.subplots(constrained_layout=True)
xs = np.linspace(LB, UB, 200)
F = lambda x: func(x, **pars)
Fs = F(xs)
ax.plot(xs, Fs, "k--")
xf = float(sol_single.vals["x"])
xs_sol = np.linspace(x0, xf, 100)
lbl = rf"$x_0={{{x0:.3f}}} \rightarrow f^{{\star}}={{{F(xf):.3f}}}$"
ax.plot(xs_sol, F(xs_sol), "-", lw=2, label=lbl, color="C0")
ax.plot(x0, F(x0), "o", markersize=6, color="C0")
ax.plot(xf, F(xf), "*", markersize=10, color="C0")
ax.set_xlabel("$x$")
ax.set_ylabel("$f(x)$")
ax.set_xlim(LB, UB)
ax.set_ylim(-1.1, 0.8)
ax.legend()
plt.show()

With csnlp.multistart#
Now, we’ll solve the same problem using the multistart capabilities of csnlp.
The submodule csnlp.multistart provides three classes to solve an NLP problem
in a multistart parallelized fashion: csnlp.multistart.StackedMultistartNlp,
csnlp.multistart.MappedMultistartNlp, and
csnlp.multistart.ParallelMultistartNlp. The interfaces of these classes
remain mostly unchanged with respect to the base class csnlp.Nlp.
The only novel is the introduction of the method
csnlp.multistart.MultistartNlp.solve_multi (which each class implements
differently), which allows to solve the problem from multiple initial conditions and
multiple parameters. The method returns the best solution found, or all solutions if
requested.
To build the multistart NLP, we follow a very similar procedure as the one above.
n_multistarts = 4
nlp = ms.StackedMultistartNlp[cs.SX](starts=n_multistarts)
# nlp = ms.MappedMultistartNlp[cs.SX](starts=n_multistarts, parallelization="thread")
# nlp = ms.ParallelMultistartNlp[cs.SX](
# starts=n_multistarts, parallel_kwargs={"n_jobs": n_multistarts}
# )
x = nlp.variable("x", lb=LB, ub=UB)[0]
p0 = nlp.parameter("p0")
p1 = nlp.parameter("p1")
nlp.minimize(func(x, p0, p1))
nlp.init_solver(opts)
The csnlp.multistart.MultistartNlp.solve_multi method can accept as inputs an
iterable of dictionaries for its arguments pars and vals0. Each dictionary
defines the conditions of a single start. However, it is also possible to pass a
single dictionary, in which case it will be used across all starts.
These iterables of dictionaries can be manually generated by the user. However,
csnlp.multistart offers classes to generate the starting points automatically
in a random or deterministic (structured) way. In what follows, we’ll generate four
starting locations while keeping the parameters fixed. Two of these locations will be
uniformly randomly generated, the other two are linearly spaced in the given domain.
random_points = ms.RandomStartPoints(
points={"x": ms.RandomStartPoint("uniform", LB, UB)},
multistarts=n_multistarts // 2,
seed=42,
)
structured_points = ms.StructuredStartPoints(
points={"x": ms.StructuredStartPoint(LB, UB)},
multistarts=n_multistarts // 2,
)
The csnlp.multistart.RandomStartPoints and
csnlp.multistart.StructuredStartPoints can be iterated over to yield the
initial conditions for each start. We’ll chain them together to pass them to the
solver (we also convert them to a list just for plotting reasons).
x0s = list(chain(random_points, structured_points))
Now, we can call the multistart solver. By passing return_all_sols=True, we are
requesting the method to return all the solutions found for each start, instead of
returning just the best one.
sol_best = nlp.solve_multi(pars=pars, vals0=x0s) # to get just the best solution
sols_multi = nlp.solve_multi(pars=pars, vals0=x0s, return_all_sols=True) # to get all
We can plot the convergence of each starting point as we did earlier.
fig, ax = plt.subplots(constrained_layout=True)
ax.plot(xs, Fs, "k--")
for i, (x0, sol) in enumerate(zip(x0s, sols_multi), start=1):
x0 = x0["x"]
xf = float(sol.vals["x"])
xs = np.linspace(x0, xf, 100)
lbl = rf"$x_0={{{x0:.3f}}} \rightarrow f^{{\star}}={{{F(xf):.3f}}}$"
ax.plot(xs, F(xs), "-", lw=2, label=lbl, color=f"C{i}")
ax.plot(x0, F(x0), "o", markersize=6, color=f"C{i}")
ax.plot(xf, F(xf), "*", markersize=10, color=f"C{i}")
ax.plot(
sol_best.vals["x"],
sol_best.f,
"s",
markersize=14,
fillstyle="none",
color="k",
label="Best",
)
x = float(sol_best.value(nlp.x))
ax.vlines(x, -1.1, sol_best.f, "k", ls="-.")
ax.hlines(sol_best.f, LB, x, "k", ls="-.")
ax.set_xlabel("$x$")
ax.set_ylabel("$f(x)$")
ax.set_xlim(LB, UB)
ax.set_ylim(-1.1, 0.8)
ax.legend()
plt.show()

Total running time of the script: (0 minutes 0.493 seconds)