1 // The libMesh Finite Element Library.
2 // Copyright (C) 2002-2020 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3 
4 // This library is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU Lesser General Public
6 // License as published by the Free Software Foundation; either
7 // version 2.1 of the License, or (at your option) any later version.
8 
9 // This library is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 // Lesser General Public License for more details.
13 
14 // You should have received a copy of the GNU Lesser General Public
15 // License along with this library; if not, write to the Free Software
16 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 
18 
19 
20 #include "libmesh/libmesh_common.h"
21 
22 #if defined(LIBMESH_HAVE_NLOPT) && !defined(LIBMESH_USE_COMPLEX_NUMBERS)
23 
24 
25 // Local Includes
26 #include "libmesh/dof_map.h"
27 #include "libmesh/libmesh_logging.h"
28 #include "libmesh/numeric_vector.h"
29 #include "libmesh/nlopt_optimization_solver.h"
30 #include "libmesh/sparse_matrix.h"
31 #include "libmesh/int_range.h"
32 #include "libmesh/utility.h"
33 
34 namespace libMesh
35 {
36 
__libmesh_nlopt_objective(unsigned n,const double * x,double * gradient,void * data)37 double __libmesh_nlopt_objective(unsigned n,
38                                  const double * x,
39                                  double * gradient,
40                                  void * data)
41 {
42   LOG_SCOPE("objective()", "NloptOptimizationSolver");
43 
44   // ctx should be a pointer to the solver (it was passed in as void *)
45   NloptOptimizationSolver<Number> * solver =
46     static_cast<NloptOptimizationSolver<Number> *> (data);
47 
48   OptimizationSystem & sys = solver->system();
49 
50   // We'll use current_local_solution below, so let's ensure that it's consistent
51   // with the vector x that was passed in.
52   for (auto i : index_range(*sys.solution))
53     sys.solution->set(i, x[i]);
54 
55   // Make sure the solution vector is parallel-consistent
56   sys.solution->close();
57 
58   // Impose constraints on X
59   sys.get_dof_map().enforce_constraints_exactly(sys);
60 
61   // Update sys.current_local_solution based on X
62   sys.update();
63 
64   Real objective;
65   if (solver->objective_object != nullptr)
66     {
67       objective =
68         solver->objective_object->objective(*(sys.current_local_solution), sys);
69     }
70   else
71     {
72       libmesh_error_msg("Objective function not defined in __libmesh_nlopt_objective");
73     }
74 
75   // If the gradient has been requested, fill it in
76   if (gradient)
77     {
78       if (solver->gradient_object != nullptr)
79         {
80           solver->gradient_object->gradient(*(sys.current_local_solution), *(sys.rhs), sys);
81 
82           // we've filled up sys.rhs with the gradient data, now copy it
83           // to the nlopt data structure
84           libmesh_assert(sys.rhs->size() == n);
85 
86           std::vector<double> grad;
87           sys.rhs->localize_to_one(grad);
88           for (unsigned int i = 0; i < n; ++i)
89             gradient[i] = grad[i];
90         }
91       else
92         libmesh_error_msg("Gradient function not defined in __libmesh_nlopt_objective");
93     }
94 
95   // Increment the iteration count.
96   solver->get_iteration_count()++;
97 
98   // Possibly print the current value of the objective function
99   if (solver->verbose)
100     libMesh::out << objective << std::endl;
101 
102   return objective;
103 }
104 
105 
106 
__libmesh_nlopt_equality_constraints(unsigned m,double * result,unsigned n,const double * x,double * gradient,void * data)107 void __libmesh_nlopt_equality_constraints(unsigned m,
108                                           double * result,
109                                           unsigned n,
110                                           const double * x,
111                                           double * gradient,
112                                           void * data)
113 {
114   LOG_SCOPE("equality_constraints()", "NloptOptimizationSolver");
115 
116   libmesh_assert(data);
117 
118   // data should be a pointer to the solver (it was passed in as void *)
119   NloptOptimizationSolver<Number> * solver =
120     static_cast<NloptOptimizationSolver<Number> *> (data);
121 
122   OptimizationSystem & sys = solver->system();
123 
124   // We'll use current_local_solution below, so let's ensure that it's consistent
125   // with the vector x that was passed in.
126   libmesh_error_msg_if(sys.solution->size() != n,
127                        "Error: Input vector x has different length than sys.solution!");
128 
129   for (auto i : index_range(*sys.solution))
130     sys.solution->set(i, x[i]);
131   sys.solution->close();
132 
133   // Impose constraints on the solution vector
134   sys.get_dof_map().enforce_constraints_exactly(sys);
135 
136   // Update sys.current_local_solution based on the solution vector
137   sys.update();
138 
139   // Call the user's equality constraints function if there is one.
140   OptimizationSystem::ComputeEqualityConstraints * eco = solver->equality_constraints_object;
141   if (eco)
142     {
143       eco->equality_constraints(*sys.current_local_solution,
144                                 *sys.C_eq,
145                                 sys);
146 
147       sys.C_eq->close();
148 
149       // Copy the values out of eq_constraints into 'result'.
150       // TODO: Even better would be if we could use 'result' directly
151       // as the storage of eq_constraints.  Perhaps a serial-only
152       // NumericVector variant which supports this option?
153       for (unsigned int i = 0; i < m; ++i)
154         result[i] = (*sys.C_eq)(i);
155 
156       // If gradient != nullptr, then the Jacobian matrix of the equality
157       // constraints has been requested.  The incoming 'gradient'
158       // array is of length m*n and d(c_i)/d(x_j) = gradient[n*i+j].
159       if (gradient)
160         {
161           OptimizationSystem::ComputeEqualityConstraintsJacobian * eco_jac =
162             solver->equality_constraints_jacobian_object;
163 
164           if (eco_jac)
165             {
166               eco_jac->equality_constraints_jacobian(*sys.current_local_solution,
167                                                      *sys.C_eq_jac,
168                                                      sys);
169 
170               sys.C_eq_jac->close();
171 
172               // copy the Jacobian data to the gradient array
173               for (numeric_index_type i=0; i<m; i++)
174                 for (const auto & dof_index : sys.eq_constraint_jac_sparsity[i])
175                   gradient[n*i+dof_index] = (*sys.C_eq_jac)(i,dof_index);
176             }
177           else
178             libmesh_error_msg("Jacobian function not defined in __libmesh_nlopt_equality_constraints");
179         }
180 
181     }
182   else
183     libmesh_error_msg("Constraints function not defined in __libmesh_nlopt_equality_constraints");
184 }
185 
186 
__libmesh_nlopt_inequality_constraints(unsigned m,double * result,unsigned n,const double * x,double * gradient,void * data)187 void __libmesh_nlopt_inequality_constraints(unsigned m,
188                                             double * result,
189                                             unsigned n,
190                                             const double * x,
191                                             double * gradient,
192                                             void * data)
193 {
194   LOG_SCOPE("inequality_constraints()", "NloptOptimizationSolver");
195 
196   libmesh_assert(data);
197 
198   // data should be a pointer to the solver (it was passed in as void *)
199   NloptOptimizationSolver<Number> * solver =
200     static_cast<NloptOptimizationSolver<Number> *> (data);
201 
202   OptimizationSystem & sys = solver->system();
203 
204   // We'll use current_local_solution below, so let's ensure that it's consistent
205   // with the vector x that was passed in.
206   libmesh_error_msg_if(sys.solution->size() != n, "Error: Input vector x has different length than sys.solution!");
207 
208   for (auto i : index_range(*sys.solution))
209     sys.solution->set(i, x[i]);
210   sys.solution->close();
211 
212   // Impose constraints on the solution vector
213   sys.get_dof_map().enforce_constraints_exactly(sys);
214 
215   // Update sys.current_local_solution based on the solution vector
216   sys.update();
217 
218   // Call the user's inequality constraints function if there is one.
219   OptimizationSystem::ComputeInequalityConstraints * ineco = solver->inequality_constraints_object;
220   if (ineco)
221     {
222       ineco->inequality_constraints(*sys.current_local_solution,
223                                     *sys.C_ineq,
224                                     sys);
225 
226       sys.C_ineq->close();
227 
228       // Copy the values out of ineq_constraints into 'result'.
229       // TODO: Even better would be if we could use 'result' directly
230       // as the storage of ineq_constraints.  Perhaps a serial-only
231       // NumericVector variant which supports this option?
232       for (unsigned int i = 0; i < m; ++i)
233         result[i] = (*sys.C_ineq)(i);
234 
235       // If gradient != nullptr, then the Jacobian matrix of the equality
236       // constraints has been requested.  The incoming 'gradient'
237       // array is of length m*n and d(c_i)/d(x_j) = gradient[n*i+j].
238       if (gradient)
239         {
240           OptimizationSystem::ComputeInequalityConstraintsJacobian * ineco_jac =
241             solver->inequality_constraints_jacobian_object;
242 
243           if (ineco_jac)
244             {
245               ineco_jac->inequality_constraints_jacobian(*sys.current_local_solution,
246                                                          *sys.C_ineq_jac,
247                                                          sys);
248 
249               sys.C_ineq_jac->close();
250 
251               // copy the Jacobian data to the gradient array
252               for (numeric_index_type i=0; i<m; i++)
253                 for (const auto & dof_index : sys.ineq_constraint_jac_sparsity[i])
254                   gradient[n*i+dof_index] = (*sys.C_ineq_jac)(i,dof_index);
255             }
256           else
257             libmesh_error_msg("Jacobian function not defined in __libmesh_nlopt_inequality_constraints");
258         }
259 
260     }
261   else
262     libmesh_error_msg("Constraints function not defined in __libmesh_nlopt_inequality_constraints");
263 }
264 
265 //---------------------------------------------------------------------
266 
267 
268 
269 //---------------------------------------------------------------------
270 // NloptOptimizationSolver<> methods
271 template <typename T>
NloptOptimizationSolver(OptimizationSystem & system_in)272 NloptOptimizationSolver<T>::NloptOptimizationSolver (OptimizationSystem & system_in)
273   :
274   OptimizationSolver<T>(system_in),
275   _opt(nullptr),
276   _result(NLOPT_SUCCESS),
277   _iteration_count(0),
278   _constraints_tolerance(1.e-8)
279 {
280   // The nlopt interfaces all use unsigned int as their index type, so
281   // don't risk using the NloptOptimizationSolver with a libmesh that
282   // is configured to use 64-bit indices. We can detect this at
283   // configure time, so it should not be possible to actually reach
284   // this error message... it's here just in case.
285   libmesh_error_msg_if(sizeof(dof_id_type) != sizeof(unsigned int),
286                        "The NloptOptimizationSolver should not be used with dof_id_type != unsigned int.");
287 }
288 
289 
290 
291 template <typename T>
~NloptOptimizationSolver()292 NloptOptimizationSolver<T>::~NloptOptimizationSolver ()
293 {
294   this->clear ();
295 }
296 
297 
298 
299 template <typename T>
clear()300 void NloptOptimizationSolver<T>::clear ()
301 {
302   if (this->initialized())
303     {
304       this->_is_initialized = false;
305 
306       nlopt_destroy(_opt);
307     }
308 }
309 
310 
311 
312 template <typename T>
init()313 void NloptOptimizationSolver<T>::init ()
314 {
315   // Initialize the data structures if not done so already.
316   if (!this->initialized())
317     {
318       this->_is_initialized = true;
319 
320       // By default, use the LD_SLSQP solver
321       std::string nlopt_algorithm_name = "LD_SLSQP";
322 
323       if (libMesh::on_command_line("--nlopt-algorithm"))
324         nlopt_algorithm_name = libMesh::command_line_next ("--nlopt-algorithm",
325                                                            nlopt_algorithm_name);
326 
327       // Convert string to an nlopt algorithm type
328       _opt = nlopt_create(libmesh_map_find(_nlopt_algorithms, nlopt_algorithm_name),
329                           this->system().solution->size());
330     }
331 }
332 
333 
334 
335 template <typename T>
solve()336 void NloptOptimizationSolver<T>::solve ()
337 {
338   LOG_SCOPE("solve()", "NloptOptimizationSolver");
339 
340   this->init ();
341 
342   unsigned int nlopt_size = this->system().solution->size();
343 
344   // We have to have an objective function
345   libmesh_assert( this->objective_object );
346 
347   // Set routine for objective and (optionally) gradient evaluation
348   {
349     nlopt_result ierr =
350       nlopt_set_min_objective(_opt,
351                               __libmesh_nlopt_objective,
352                               this);
353     libmesh_error_msg_if(ierr < 0, "NLopt failed to set min objective: " << ierr);
354   }
355 
356   if (this->lower_and_upper_bounds_object)
357     {
358       // Need to actually compute the bounds vectors first
359       this->lower_and_upper_bounds_object->lower_and_upper_bounds(this->system());
360 
361       std::vector<Real> nlopt_lb(nlopt_size);
362       std::vector<Real> nlopt_ub(nlopt_size);
363       for (unsigned int i = 0; i < nlopt_size; ++i)
364         {
365           nlopt_lb[i] = this->system().get_vector("lower_bounds")(i);
366           nlopt_ub[i] = this->system().get_vector("upper_bounds")(i);
367         }
368 
369       nlopt_set_lower_bounds(_opt, nlopt_lb.data());
370       nlopt_set_upper_bounds(_opt, nlopt_ub.data());
371     }
372 
373   // If we have an equality constraints object, tell NLopt about it.
374   if (this->equality_constraints_object)
375     {
376       // NLopt requires a vector to specify the tolerance for each constraint.
377       // NLopt makes a copy of this vector internally, so it's safe for us to
378       // let it go out of scope.
379       std::vector<double> equality_constraints_tolerances(this->system().C_eq->size(),
380                                                           _constraints_tolerance);
381 
382       // It would be nice to call the C interface directly, at least it should return an error
383       // code we could parse... unfortunately, there does not seem to be a way to extract
384       // the underlying nlopt_opt object from the nlopt::opt class!
385       nlopt_result ierr =
386         nlopt_add_equality_mconstraint(_opt,
387                                        equality_constraints_tolerances.size(),
388                                        __libmesh_nlopt_equality_constraints,
389                                        this,
390                                        equality_constraints_tolerances.data());
391 
392       libmesh_error_msg_if(ierr < 0, "NLopt failed to add equality constraint: " << ierr);
393     }
394 
395   // If we have an inequality constraints object, tell NLopt about it.
396   if (this->inequality_constraints_object)
397     {
398       // NLopt requires a vector to specify the tolerance for each constraint
399       std::vector<double> inequality_constraints_tolerances(this->system().C_ineq->size(),
400                                                             _constraints_tolerance);
401 
402       nlopt_add_inequality_mconstraint(_opt,
403                                        inequality_constraints_tolerances.size(),
404                                        __libmesh_nlopt_inequality_constraints,
405                                        this,
406                                        inequality_constraints_tolerances.data());
407     }
408 
409   // Set a relative tolerance on the optimization parameters
410   nlopt_set_ftol_rel(_opt, this->objective_function_relative_tolerance);
411 
412   // Set the maximum number of allowed objective function evaluations
413   nlopt_set_maxeval(_opt, this->max_objective_function_evaluations);
414 
415   // Reset internal iteration counter
416   this->_iteration_count = 0;
417 
418   // Perform the optimization
419   std::vector<Real> x(nlopt_size);
420   Real min_val = 0.;
421   _result = nlopt_optimize(_opt, x.data(), &min_val);
422 
423   if (_result < 0)
424     libMesh::out << "NLopt failed!" << std::endl;
425   else
426     libMesh::out << "NLopt obtained optimal value: "
427                  << min_val
428                  << " in "
429                  << this->get_iteration_count()
430                  << " iterations."
431                  << std::endl;
432 }
433 
434 
435 template <typename T>
print_converged_reason()436 void NloptOptimizationSolver<T>::print_converged_reason()
437 {
438   libMesh::out << "NLopt optimization solver convergence/divergence reason: "
439                << this->get_converged_reason() << std::endl;
440 }
441 
442 
443 
444 template <typename T>
get_converged_reason()445 int NloptOptimizationSolver<T>::get_converged_reason()
446 {
447   return static_cast<int>(_result);
448 }
449 
450 
451 //------------------------------------------------------------------
452 // Explicit instantiations
453 template class NloptOptimizationSolver<Number>;
454 
455 } // namespace libMesh
456 
457 
458 
459 #endif // #if defined(LIBMESH_HAVE_NLOPT) && !defined(LIBMESH_USE_COMPLEX_NUMBERS)
460