Source code for modopt.external_libraries.pyslsqp.pyslsqp

import numpy as np
from modopt import Optimizer
import time
from modopt.utils.options_dictionary import OptionsDictionary
from typing import Callable

[docs]class PySLSQP(Optimizer): ''' Class that interfaces modOpt with the PySLSQP package which is a Python wrapper for the SLSQP optimization algorithm. PySLSQP can solve nonlinear programming problems with equality and inequality constraints. Parameters ---------- problem : Problem or ProblemLite Object containing the problem to be solved. recording : bool, default=False If ``True``, record all outputs from the optimization. This needs to be enabled for hot-starting the same problem later, if the optimization is interrupted. out_dir : str, optional The directory to store all the output files generated from the optimization. hot_start_from : str, optional The record file from which to hot-start the optimization. hot_start_atol : float, default=0. The absolute tolerance check for the inputs when reusing outputs from the hot-start record. hot_start_rtol : float, default=0. The relative tolerance check for the inputs when reusing outputs from the hot-start record. visualize : list, default=[] The list of scalar variables to visualize during the optimization. keep_viz_open : bool, default=False If ``True``, keep the visualization window open after the optimization is complete. turn_off_outputs : bool, default=False If ``True``, prevent modOpt from generating any output files. solver_options : dict, default={} Dictionary containing the options to be passed to the solver. Available options are: 'maxiter', 'acc', 'iprint', 'callback', 'summary_filename', 'visualize', 'visualize_vars', 'keep_plot_open', 'save_figname', 'save_itr', 'save_vars', 'save_filename', 'load_filename', 'warm_start', 'hot_start'. See the PySLSQP page in modOpt's documentation for more information. readable_outputs : list, default=[] List of outputs to be written to readable text output files. Available outputs are: 'x'. ''' def initialize(self, ): self.solver_name = 'pyslsqp' self.options.declare('solver_options', types=dict, default={}) self.default_solver_options = { 'maxiter': (int, 100), 'acc': (float, 1e-6), 'iprint': (int, 1), 'callback': ((type(None), Callable), None), 'summary_filename': (str, 'slsqp_summary.out'), 'visualize': (bool, False), 'visualize_vars': (list, ['objective', 'optimality', 'feasibility']), 'keep_plot_open': (bool, False), 'save_figname': (str, 'slsqp_plot.pdf'), 'save_itr': ((type(None), str), None), 'save_vars': (list, ['x', 'objective', 'optimality', 'feasibility', 'step', 'iter', 'majiter', 'ismajor', 'mode']), 'save_filename': (str, 'slsqp_recorder.hdf5'), 'load_filename': ((type(None), str), None), 'warm_start': (bool, False), 'hot_start': (bool, False), } # Used for verifying the keys and value-types of user-provided solver_options, # and generating an updated pure Python dictionary to provide pyslsqp.optimize() self.solver_options = OptionsDictionary() for key, value in self.default_solver_options.items(): self.solver_options.declare(key, types=value[0], default=value[1]) # Declare outputs self.available_outputs = {'x': (float, (self.problem.nx,)),} self.options.declare('readable_outputs', values=([],['x']), default=[]) self.x0 = self.problem.x0 * 1. self.nx = self.problem.nx * 1 self.obj = self.problem._compute_objective self.grad = self.problem._compute_objective_gradient self.active_callbacks = ['obj', 'grad'] if self.problem.constrained: self.con_in = self.problem._compute_constraints self.jac_in = self.problem._compute_constraint_jacobian self.active_callbacks += ['con', 'jac'] def setup(self): ''' Setup the initial guess, and matrices and vectors. ''' # Check if user-provided solver_options have valid keys and value-types self.solver_options.update(self.options['solver_options']) self.options_to_pass = self.solver_options.get_pure_dict() self.user_callback = self.options_to_pass.pop('callback') if hasattr(self, 'out_dir'): self.options_to_pass['summary_filename'] = self.out_dir + '/' + self.options_to_pass['summary_filename'] self.options_to_pass['save_filename'] = self.out_dir + '/' + self.options_to_pass['save_filename'] if self.problem.constrained: self.setup_constraints() def setup_constraints(self, ): cl = self.problem.c_lower cu = self.problem.c_upper eqi = self.eq_constraint_indices = np.where(cl == cu)[0] lci = self.lower_constraint_indices = np.where((cl != -np.inf) & (cl != cu))[0] uci = self.upper_constraint_indices = np.where((cu != np.inf) & (cl != cu))[0] self.eq_constrained = True if len(eqi) > 0 else False self.lower_constrained = True if len(lci) > 0 else False self.upper_constrained = True if len(uci) > 0 else False self.nc_e = len(eqi) self.nc_i = len(lci) + len(uci) self.nc = self.nc_e + self.nc_i if self.nc == 0: raise ValueError('No constraint bounds found, but problem.constrained is set as True.') def con(self, x): c_in = self.con_in(x) eqi = self.eq_constraint_indices lci = self.lower_constraint_indices uci = self.upper_constraint_indices nc_e = self.nc_e c = np.zeros(self.nc) if self.eq_constrained: c[:nc_e] = c_in[eqi] - self.problem.c_lower[eqi] if self.lower_constrained: c[nc_e:nc_e + len(lci)] = c_in[lci] - self.problem.c_lower[lci] if self.upper_constrained: c[nc_e + len(lci):] = self.problem.c_upper[uci] - c_in[uci] return c def jac(self, x): j_in = self.jac_in(x) eqi = self.eq_constraint_indices lci = self.lower_constraint_indices uci = self.upper_constraint_indices nc_e = self.nc_e j = np.zeros((self.nc, self.nx)) if self.eq_constrained: j[:nc_e] = j_in[eqi] if self.lower_constrained: j[nc_e:nc_e + len(lci)] = j_in[lci] if self.upper_constrained: j[nc_e + len(lci):] = -j_in[uci] return j
[docs] def solve(self): try: from pyslsqp import optimize except ImportError: raise ImportError("PySLSQP could not be imported or is not installed. Install it with 'pip install pyslsqp'.") # Set up the problem x0 = self.x0 obj = self.obj grad = self.grad if self.problem.constrained: con = self.con jac = self.jac else: con = None jac = None xl = self.problem.x_lower xu = self.problem.x_upper meq = self.nc_e if self.problem.constrained else 0 def callback(x): self.update_outputs(x=x) if self.user_callback: self.user_callback(x) self.update_outputs(x=self.x0) # Run the optimization start_time = time.time() self.results = optimize(x0, obj=obj, con=con, grad=grad, jac=jac, xl=xl, xu=xu, meq=meq, callback=callback, **self.options_to_pass) self.total_time = time.time() - start_time self.run_post_processing() return self.results
[docs] def print_results(self, optimal_variables=False, optimal_gradient=False, optimal_constraints=False, optimal_jacobian=False, optimal_multipliers=False, all=False): ''' Print the optimization results to the console. Parameters ---------- optimal_variables : bool, default=False If ``True``, print the optimal variables. optimal_gradient : bool, default=False If ``True``, print the optimal objective gradient. optimal_constraints : bool, default=False If ``True``, print the optimal constraints. optimal_jacobian : bool, default=False If ``True``, print the optimal constraint Jacobian. optimal_multipliers : bool, default=False If ``True``, print the optimal multipliers. all : bool, default=False If ``True``, print all available information. ''' output = "\n\tSolution from PySLSQP:" output += "\n\t"+"-" * 100 output += f"\n\t{'Problem':30}: {self.problem_name}" output += f"\n\t{'Solver':30}: {self.solver_name}" output += f"\n\t{'Success':30}: {self.results['success']}" output += f"\n\t{'Message':30}: {self.results['message']}" output += f"\n\t{'Status':30}: {self.results['status']}" output += f"\n\t{'Objective':30}: {self.results['objective']}" output += f"\n\t{'Optimality':30}: {self.results['optimality']}" output += f"\n\t{'Feasibility':30}: {self.results['feasibility']}" output += f"\n\t{'Gradient norm':30}: {np.linalg.norm(self.results['gradient'])}" output += f"\n\t{'Major iterations':30}: {self.results['num_majiter']}" output += f"\n\t{'Total function evals':30}: {self.results['nfev']}" output += f"\n\t{'Total gradient evals':30}: {self.results['ngev']}" output += f"\n\t{'Total time':30}: {self.total_time}" output += f"\n\t{'Function eval. time':30}: {self.results['fev_time']}" output += f"\n\t{'Derivative eval. time':30}: {self.results['gev_time']}" output += f"\n\t{'Optimizer time':30}: {self.results['optimizer_time']}" output += f"\n\t{'Processing time':30}: {self.results['processing_time']}" output += f"\n\t{'Visualization time':30}: {self.results['visualization_time']}" output += f"\n\t{'Summary saved in':30}: {self.results['summary_filename']}" if 'save_filename' in self.results.keys(): output += f"\n\t{'Iteration data saved in':30}: {self.results['save_filename']}" if 'plot_filename' in self.results.keys(): output += f"\n\t{'Plot saved in':30}: {self.results['plot_filename']}" if 'nfev_reused_in_hotstart' in self.results.keys(): output += f"\n\t{'Fun. evals reused (hotstart)':30}: {self.results['nfev_reused_in_hotstart']}" if 'ngev_reused_in_hotstart' in self.results.keys(): output += f"\n\t{'Der. evals reused (hotstart)':30}: {self.results['ngev_reused_in_hotstart']}" output += self.get_callback_counts_string(30) if optimal_variables or all: output += f"\n\t{'Optimal variables':30}: {self.results['x']}" if optimal_gradient or all: output += f"\n\t{'Optimal obj. gradient':30}: {self.results['gradient']}" if optimal_constraints or all: output += f"\n\t{'Optimal constraints':30}: {self.results['constraints']}" if optimal_multipliers or all: output += f"\n\t{'Optimal multipliers (constr.)':30}: {self.results['multipliers']}" if optimal_jacobian or all: output += f"\n\t{'Optimal Jacobian':30}: {self.results['jacobian']}" output += '\n\t' + '-'*100 print(output)