import numpy as np
import scipy as sp
from modopt import Problem as Problem
from modopt.core.recording_and_hotstart import hot_start, record
# Note: modopt.CUTEstProblem is different from pycutest.CUTEstProblem
[docs]class CUTEstProblem(Problem):
'''
Class that wraps pyCUTEst problems for modOpt.
General functionality pyCUTEst provides:
- `find_problems()`: Find problems that satisfy given criteria as per CUTEst classification scheme.
- `problem_properties()`: Get properties of a given problem.
- `print_available_sif_params()`: Print available optional input parameters for a given problem.
- `import_problem()`: Import a given problem with optional parameters. Returns a ``pycutest.CUTEstProblem`` object.
Examples
--------
Import a CUTEst problem named `'ROSENBR'` and solve it using modOpt
>>> import pycutest
>>> cutest_problem = pycutest.import_problem('ROSENBR')
>>> import modopt as mo
>>> problem = mo.CUTEstProblem(cutest_problem=cutest_problem)
>>> optimizer = mo.SLSQP(problem, solver_options={'maxiter':100})
>>> optimizer.solve()
**Methods**
'''
def initialize(self, ):
'''
Initialize the Problem() instance for a CUTEstProblem.
'''
try:
import pycutest
except ImportError:
raise ImportError("'pycutest' could not be imported")
self.options.declare('cutest_problem', types=pycutest.CUTEstProblem)
def setup(self, ):
'''
Setup the problem name, initial guess, and problem dimensions.
'''
import pycutest
prob = self.options['cutest_problem']
# Set problem_name
# (Since it is not constructed explicitly using the Problem() class)
self.problem_name = prob.name
self.problem_properties = pycutest.problem_properties(self.problem_name)
self.x0 = prob.x0 * 1.
# Set problem dimensions
self.nx = prob.n
self.nc = prob.m
if self.nc > 0:
self.constrained = True
self.model_evals = 0 # number of function evaluations
self.deriv_evals = 0 # number of derivative evaluations
# self.fail1 = False # failure of functions
# self.fail2 = False # failure of functions or derivatives
self.warm_x = self.x0 - 1. # (x0 - 1.) to keep it different from initial dv values
self.warm_x_deriv = self.x0 - 2. # (x0 - 2.) to keep it different from initial dv and warm_x values
# Other attributes available in pycutest.CUTEstProblem
self.sifParams = prob.sifParams # dict of parameters passed to sifdecode
self.sifOptions = prob.sifOptions # list of extra options passed to sifdecode
self.vartype = prob.vartype # array of variable types (NumPy array size n, entry vartype[i]
# indicates that x[i] is real(0), boolean(1), or integer(2))
self.nnzh = prob.nnzh # number of nonzero entries in upper triangular part of objective Hessian
# (for all variables, including fixed)
self.nonlinear_vars_first = prob.nonlinear_vars_first # flag if all nonlinear variables are listed before linear variables
self.n_full = prob.n_full # total number of variables in CUTEst problem (n_free + n_fixed)
self.n_free = prob.n_free # number of free variables in CUTEst problem
self.n_fixed = prob.n_fixed # number of fixed variables in CUTEst problem
self.o_scaler = self.x_scaler = 1.0
self.c_scaler = None
if self.constrained:
self.eq_cons_first = prob.eq_cons_first # flag if all equality constraints are listed before inequality constraints
self.linear_cons_first = prob.linear_cons_first # flag if all linear constraints are listed before nonlinear constraints
self.nnzj = prob.nnzj # number of nonzero entries in constraint Jacobian (for all variables, including fixed)
self.v0 = prob.v0 # initial values of the Lagrange multipliers (np.array of shape (m,))
self.is_eq_cons = prob.is_eq_cons # shape (m,) np.array of Boolean flags indicating if i-th constraint is
# equality or not (i.e. inequality)
self.is_linear_cons = prob.is_linear_cons # shape (m,) np.array of Boolean flags indicating if i-th constraint is
# linear or not (i.e. nonlinear)
# Create list of user-defined callbacks
self.user_defined_callbacks = [] # list of user-defined callbacks
if self.problem_properties['objective'] != 'none':
self.user_defined_callbacks += ['obj']
if self.problem_properties['degree'] > 0:
self.user_defined_callbacks += ['grad']
if self.problem_properties['degree'] > 1:
self.user_defined_callbacks += ['obj_hess']
if not self.constrained:
self.user_defined_callbacks += ['obj_hvp']
if self.constrained:
self.c_scaler = 1.0
self.user_defined_callbacks += ['con']
self.user_defined_callbacks += ['lag']
if self.problem_properties['degree'] > 0:
self.user_defined_callbacks += ['jac']
self.user_defined_callbacks += ['lag_grad']
self.user_defined_callbacks += ['jvp']
self.user_defined_callbacks += ['vjp']
if self.problem_properties['degree'] > 1:
self.user_defined_callbacks += ['lag_hess']
self.user_defined_callbacks += ['lag_hvp']
self.declared_variables = ['dv'] + self.user_defined_callbacks
def setup_derivatives(self):
pass
def raise_issues_with_user_setup(self, ):
pass
def _setup_scalers(self, ):
pass
def _setup_bounds(self):
'''
Setup the variable and constraint bounds for the modOpt problem.
'''
prob = self.options['cutest_problem']
# Set design variable bounds
x_l = prob.bl * 1.
x_u = prob.bu * 1.
self.x_lower = np.where(x_l == -1.0e20, -np.inf, x_l)
self.x_upper = np.where(x_u == 1.0e20, np.inf, x_u)
# Set constraint bounds
if self.nc > 0:
self.c_lower = np.where(prob.cl == -1.0e20, -np.inf, prob.cl)
self.c_upper = np.where(prob.cu == 1.0e20, np.inf, prob.cu)
else:
self.c_lower = None
self.c_upper = None
# TODO: Add decorators for checking if x is warm and for updating dvs
# TODO: Add jvp, hvp, laggrad, laghess, etc. for CUTEst problems
[docs] @record(['x'], ['obj'])
@hot_start(['x'], ['obj'])
def _compute_objective(self, x, force_rerun=False, check_failure=False):
'''
Compute and return the objective 'f' for the given design variable vector 'x'.
'''
prob = self.options['cutest_problem']
if not np.array_equal(self.warm_x, x):
# for unconstrained problems, objcons() returns (f, None)
self.f, self.c = prob.objcons(x)
self.warm_x = x * 1.
self.model_evals += 1
return self.f
# return failure_flag, sim.objective()
[docs] @record(['x'], ['grad'])
@hot_start(['x'], ['grad'])
def _compute_objective_gradient(self, x, force_rerun=False, check_failure=False):
'''
Compute and return the objective gradient 'g' for the given design variable vector 'x'.
'''
prob = self.options['cutest_problem']
if not np.array_equal(self.warm_x_deriv, x):
self.g, self.j = prob.lagjac(x)
self.warm_x_deriv = x * 1.
self.deriv_evals += 1
return self.g
[docs] @record(['x'], ['con'])
@hot_start(['x'], ['con'])
def _compute_constraints(self, x, force_rerun=False, check_failure=False):
'''
Compute and return the constraints 'c' for the given design variable vector 'x'.
'''
prob = self.options['cutest_problem']
if not np.array_equal(self.warm_x, x):
self.f, self.c = prob.objcons(x)
self.warm_x = x * 1.
self.model_evals += 1
return self.c
[docs] @record(['x'], ['jac'])
@hot_start(['x'], ['jac'])
def _compute_constraint_jacobian(self, x, force_rerun=False, check_failure=False):
'''
Compute and return the constraint Jacobian 'j' for the given design variable vector 'x'.
'''
prob = self.options['cutest_problem']
if not np.array_equal(self.warm_x_deriv, x):
# for unconstrained problems, lagjac() returns (g, None)
self.g, self.j = prob.lagjac(x)
self.warm_x_deriv = x * 1.
self.deriv_evals += 1
return self.j
[docs] @record(['x'], ['failure', 'obj', 'con', 'grad', 'jac'])
@hot_start(['x'], ['failure', 'obj', 'con', 'grad', 'jac'])
def _compute_all(self, x, force_rerun=False, check_failure=False):
'''
Compute and return the objective, constraints, objective gradient,
and constraint Jacobian for the given design variable vector.
Parameters
----------
x: np.ndarray
Vector of design variable values.
Returns
-------
self.f : float
Objective value.
self.c : np.ndarray
Constraint vector.
self.g : np.ndarray
Objective gradient vector.
self.j : np.ndarray
Constraint Jacobian vector.
'''
prob = self.options['cutest_problem']
if not np.array_equal(self.warm_x, x): # here, warm_x = warm_x_deriv
self.f, self.c = prob.objcons(x)
self.g, self.j = prob.lagjac(x)
self.warm_x = x * 1.
self.warm_x_deriv = x * 1.
self.model_evals += 1
self.deriv_evals += 1
return False, self.f, self.c, self.g, self.j
[docs] @record(['x'], ['obj_hess'])
@hot_start(['x'], ['obj_hess'])
def _compute_objective_hessian(self, x):
'''
Compute and return the objective Hessian 'h' for the given design variable vector 'x'.
'''
prob = self.options['cutest_problem']
self.h = prob.ihess(x)
return self.h
[docs] @record(['x', 'mu'], ['lag_hess'])
@hot_start(['x', 'mu'], ['lag_hess'])
def _compute_lagrangian_hessian(self, x, mu):
'''
Compute and return the Lagrangian Hessian 'lh' for the given design variable vector 'x' and Lagrange multipliers 'mu'.
'''
prob = self.options['cutest_problem']
self.lh = prob.hess(x, mu)
return self.lh
[docs] @record(['x', 'mu'], ['lag'])
@hot_start(['x', 'mu'], ['lag'])
def _compute_lagrangian(self, x, mu):
'''
Compute and return the Lagrangian 'l' for the given design variable vector 'x' and Lagrange multipliers 'mu'.
'''
prob = self.options['cutest_problem']
self.l = prob.lag(x, mu)
return self.l
[docs] @record(['x', 'mu'], ['lag_grad'])
@hot_start(['x', 'mu'], ['lag_grad'])
def _compute_lagrangian_gradient(self, x, mu):
'''
Compute and return the Lagrangian gradient 'lg' for the given design variable vector 'x' and Lagrange multipliers 'mu'.
'''
prob = self.options['cutest_problem']
g, self.lg = prob.lagjac(x, mu)
return self.lg
[docs] @record(['x', 'v'], ['jvp'])
@hot_start(['x', 'v'], ['jvp'])
def _compute_constraint_jvp(self, x, v):
'''
Compute and return the Jacobian-vector product 'jvp' for the given design variable vector 'x' and vector 'v'.
'''
prob = self.options['cutest_problem']
self.jvp = prob.jprod(v, x=x)
return self.jvp
[docs] @record(['x', 'v'], ['vjp'])
@hot_start(['x', 'v'], ['vjp'])
def _compute_constraint_vjp(self, x, v):
'''
Compute and return the vector-Jacobian product 'vjp' for the given design variable vector 'x' and vector 'v'.
'''
prob = self.options['cutest_problem']
self.vjp = prob.jprod(v, x=x, transpose=True)
return self.vjp
[docs] @record(['x', 'v'], ['obj_hvp'])
@hot_start(['x', 'v'], ['obj_hvp'])
def _compute_objective_hvp(self, x, v):
'''
Compute and return the objective Hessian-vector product 'hvp' for the given design variable vector 'x' and vector 'v'.
Only works for UNCONSTRAINED problems. (v must be None for unconstrained problems)
'''
if self.constrained:
raise ValueError("Objective Hessian-vector product is not defined for constrained CUTEST problems."\
"Use '_compute_lagrangian_hvp' for constrained problems.")
prob = self.options['cutest_problem']
self.hvp = prob.hprod(v, x=x, v=None)
return self.hvp
[docs] @record(['x', 'mu', 'v'], ['lag_hvp'])
@hot_start(['x', 'mu', 'v'], ['lag_hvp'])
def _compute_lagrangian_hvp(self, x, mu, v):
'''
Compute and return the Lagrangian Hessian-vector product 'lhvp' for the given design variable vector 'x', Lagrange multipliers 'mu', and vector 'v'.
Only works for CONSTRAINED problems. (v=mu must be specified for constrained problems)
'''
if not self.constrained:
raise ValueError("Lagrangian Hessian-vector product is not defined for unconstrained CUTEST problems."\
"Use '_compute_objective_hvp' for unconstrained problems.")
prob = self.options['cutest_problem']
self.lhvp = prob.hprod(v, x=x, v=mu)
return self.lhvp
def get_usage_statistics(self):
'''
Get the usage statistics for the problem.
'''
prob = self.options['cutest_problem']
stats = prob.report()
self.noev = stats['f'] # number of objective evaluations
self.ncev = stats['c'] # number of constraint evaluations
self.nogev = stats['g'] # number of objective gradient evaluations
self.ncgev = stats['cg'] # number of constraint gradient evaluations
self.nohev = stats['H'] # number of objective Hessian evaluations
self.nchev = stats['cH'] # number of constraint Hessian evaluations
self.nohvpev = stats['Hprod'] # number of objective Hessian-vector product evaluations
self.setup_time = stats['tsetup'] # CPU time for setup
self.run_time = stats['trun'] # CPU time for run
self.nfev = self.model_evals # number of function evaluations
self.ngev = self.deriv_evals # number of derivative evaluations
def find_problems(objective=None, constraints=None, regular=None, degree=None, origin=None, internal=None, n=None, userN=None, m=None, userM=None):
'''
Returns a list of problem names for problems that satisfy the criteria provided.
See CUTE classification scheme for a detailed description of the criteria.
(http://www.cuter.rl.ac.uk/Problems/classification.shtml)
This function just wraps the pycutest function `pycutest.find_problems()`.
.. note::
Problems with a user-settable number of variables/constraints will be match any given n / m.
Warnings
--------
If a requirement is not provided, it is not applied. See below for details on the requirements.
Parameters
----------
objective: str
A string containing one or more substrings
(``'none'``, ``'constant'``, ``'linear'``, ``'quadratic'``, ``'sum of squares'``, ``'other'``)
specifying the type of the objective function.
constraints: str
A string containing one or more substrings
(``'unconstrained'``, ``'fixed'``, ``'bound'``, ``'adjacency'``, ``'linear'``, ``'quadratic'``, ``'other'``)
specifying the type of the constraints.
regular: bool
``True`` if the problem must be regular, ``False`` if it must be irregular.
degree: list
A list of the form ``[min, max]`` specifying the minimum and the maximum number of
analytically available derivatives.
origin: str
A string containing one or more substrings (``'academic'``, ``'modelling'``, ``'real-world'``)
specifying the origin of the problem.
internal: bool
A boolean, ``True`` if the problem must have internal variables,
``False`` if internal variables are not allowed.
n: list
A list of the form ``[min, max]`` specifying the lowest and the highest allowed number of variables.
userN: bool
``True`` if the problems must have user settable number of variables,
``False`` if the number must be hardcoded.
m: list
A list of the form ``[min, max]`` specifying the lowest and the highest allowed number of constraints.
userM: bool
``True`` if the problems must have user settable number of variables,
``False`` if the number must be hardcoded.
Returns
-------
prob_names: list
A list of strings with problem names which satisfy the given requirements.
Examples
--------
# Choose unconstrained, variable-dimension problems
>>> probs = modopt.cutest.find_problems(constraints='unconstrained', userN=True)
'''
import pycutest
return pycutest.find_problems(objective=objective, constraints=constraints, regular=regular, degree=degree,
origin=origin, internal=internal, n=n, userN=userN, m=m, userM=userM)
def problem_properties(problem_name):
'''
Returns problem properties (based on the CUTEst problem classification string).
See http://www.cuter.rl.ac.uk/Problems/classification.shtml for details on the properties.
This function just wraps the pycutest function `pycutest.problem_properties()`.
The output is a dictionary with the following members:
* objective: objective type (e.g., linear, quadratic, ...)
* constraints: constraints type (e.g., unconstrained, variable bounds only, ...)
* regular: ``True`` if the problem's first and second derivatives exist and are continuous everywhere
* degree: highest degree of analytically available derivatives (restricted to 0, 1, or 2)
* origin: problem origin (can be academic, modelling, or real-world problems)
* internal: ``True`` if problem has explicit internal variables
* n: number of variables (='variable' if it can be set by the user)
* m: number of constraints (='variable' if it can be set by the user)
Parameters
----------
problem_name: str
Name of the CUTEst problem for which you need the problem properties.
Returns
-------
problem_properties: dict
A dictionary containing given problem's properties.
Examples
--------
# Properties of problem ROSENBR
>>> probs = modopt.cutest.problem_properties('ROSENBR')
'''
import pycutest
return pycutest.problem_properties(problem_name)
def print_available_sif_params(problem_name):
'''
This function calls sifdecode on a given problem to print out available optional input parameters.
This function is OS dependent, and it currently works only on Linux and MacOS.
This function just wraps the pycutest function `pycutest.print_available_sif_params()`.
Parameters
----------
problem_name: str
Name of the CUTEst problem for which you need to print the sif parameters.
Examples
--------
# Print optional sif parameters for problem ARGLALE
>>> probs = modopt.cutest.print_available_sif_params('ARGLALE')
'''
import pycutest
pycutest.print_available_sif_params(problem_name)
def import_problem(problemName, destination=None, sifParams=None, sifOptions=None, efirst=False, lfirst=False, nvfirst=False, quiet=True, drop_fixed_variables=True):
'''
Prepares a problem interface module, imports and initializes it (all done by pycutest).
This function wraps the pycutest function `pycutest.import_problem()`.
Parameters
----------
problemName: str
CUTEst problem name.
destination: str
The name under which the compiled problem interface is stored in the cache (default = ``problemName``).
sifParams: dict
SIF file parameters to use (as dict, keys must be strings).
sifOptions: list
Additional options passed to sifdecode given in the form of a list of strings.
efirst: bool
Order equation constraints first (default ``True``).
lfirst: bool
Order linear constraints first (default ``True``).
nvfirst: bool
Order nonlinear variables before linear variables (default ``False``).
quiet: bool
Suppress output (default ``True``).
drop_fixed_variables: bool
in the resulting problem object, are fixed variables hidden from the user (default ``True``).
Parameters
----------
pycutest_problem: pycutest.CUTEstProblem
A reference to the Python interface object for given problem_name, sifParams,
and other arguments provided.
Examples
--------
# Build the problem named ARGLALE with sif parameters N=100, M=200
>>> problem = modopt.cutest.import_problem('ARGLALE', sifParams={'N':100, 'M':200})
>>> print(problem)
'''
import pycutest
try:
return pycutest.import_problem(problemName, destination=destination, sifParams=sifParams, sifOptions=sifOptions,
efirst=efirst, lfirst=lfirst, nvfirst=nvfirst, quiet=quiet,
drop_fixed_variables=drop_fixed_variables)
except RuntimeError as e:
if 'SIFDECODE error: Failed setting' in str(e):
print_available_sif_params(problemName)
raise RuntimeError(str(e) + ' for problem ' + problemName)
else:
raise RuntimeError(str(e))