from modopt.utils.options_dictionary import OptionsDictionary
from modopt.utils.general_utils import pad_name
from modopt.core.recording_and_hotstart import record, hot_start
import numpy as np
import warnings
import time
COMPUTE_NOT_IMPLEMENTED_ERROR = (
"Unlike Problem, 'compute_*' methods are not available in ProblemLite because they are user-defined. "
"Only '_compute_*' methods are implemented in ProblemLite and callable."
)
[docs]class ProblemLite(object):
'''
Lightweight base class for defining optimization problems in modOpt.
Performs basic setup for optimization problems, without using ``array_manager``.
This class is useful for defining simple optimization problems
with initial design variables, objective, constraints and their derivative functions.
The ``ProblemLite()`` object can be used when the user wants to call the optimizer directly with
x0, obj, con, grad, jac, obj_hess, etc. functions.
Major differences from ``Problem()`` class:
- No ``array_manager`` objects are used so no setup of matrices, vectors, etc.
- No declarations of design variables, objectives, constraints, etc.
- No ``setup()`` or ``setup_derivatives()`` method is called.
- Functions and derivatives are directly called from the user-provided functions (thin wrapper).
- Only single objective problems are supported.
- Only a single design variable vector and single constraint vector function are supported.
- Every STORED VARIABLE in this class is SCALED by the user-provided scaling factors.
- Objective and constraint functions are always called together in ``_funcs(x)`` method.
- Gradient and Jacobian functions are always called together in ``_derivs(x)`` method.
- Caches the function and first derivative values for the same input ``x`` to avoid redundant, consecutive evaluations.
- Keeps track of the number of function and gradient evaluations and time taken for each.
Still supports:
- Feasibility problems and unconstrained optimization problems.
- Finite differencing by default for unavailable derivatives.
- Matrix-vector products vjp, jvp, obj_hvp, lag_hvp.
- Caching of function and derivative values (although scaled) for the same input ``x``.
- Scaling of design variables, objectives, and constraints.
- Bounds on design variables and constraints.
- Lagrangian functions and derivatives for constrained problems.
- Sparse or dense matrix formats for Jacobian and Hessian in user-provided functions,
if supported by the optimizer used.
'''
[docs] def __init__(self, x0, name='unnamed_problem', obj=None, con=None, grad=None, jac=None, obj_hess=None, lag_hess=None,
fd_step=1e-6, vp_fd_step=1e-6, xl=None, xu=None, cl=None, cu=None, x_scaler=1., o_scaler=1., c_scaler=1.,
jvp=None, vjp=None, obj_hvp=None, lag_hvp=None, lag=None, lag_grad=None, grad_free=False):
'''
Initialize the optimization problem with the given design variables, objective, constraints, and their derivatives.
Attributes
----------
name : str, default='unnamed_problem'
Problem name assigned by the user.
x0 : np.ndarray
Initial guess for design variables.
obj : callable
Objective function.
Signature: obj(x: np.ndarray) -> float
con : callable
Constraints function.
Signature: con(x: np.ndarray) -> np.ndarray
grad : callable
Gradient of the objective function.
Signature: grad(x: np.ndarray) -> np.ndarray
jac : callable
Jacobian of the constraints function.
Signature: jac(x: np.ndarray) -> np.ndarray
obj_hess : callable
Hessian of the objective function.
Signature: obj_hess(x: np.ndarray) -> np.ndarray
lag_hess : callable
Hessian of the Lagrangian function.
Signature: lag_hess(x: np.ndarray, mu: np.ndarray) -> np.ndarray
fd_step : float or np.ndarray, default=1e-6
Finite difference step size for gradient, Jacobian, and Hessian computations.
vp_fd_step : float, default=1e-6
Finite difference step size for computing vector products, if not provided by the user.
USed in JVP, OBJ_HVP, LAG_HVP computations. Must always be a scalar.
xl : float or np.ndarray
Lower bounds on design variables.
xu : float or np.ndarray
Upper bounds on design variables.
cl : float or np.ndarray
Lower bounds on constraints.
cu : float or np.ndarray
Upper bounds on constraints.
x_scaler : float or np.ndarray
Scaling factor for design variables.
o_scaler : float
Scaling factor for the objective function.
c_scaler : float or np.ndarray
Scaling factor for constraints.
jvp : callable
Jacobian-vector product function.
Signature: jvp(x: np.ndarray, v: np.ndarray) -> np.ndarray
vjp : callable
Vector-Jacobian product function.
Signature: vjp(x: np.ndarray, v: np.ndarray) -> np.ndarray
obj_hvp : callable
Hessian-vector product function for the objective.
Signature: obj_hvp(x: np.ndarray, v: np.ndarray) -> np.ndarray
lag_hvp : callable
Hessian-vector product function for the Lagrangian.
Signature: lag_hvp(x: np.ndarray, mu: np.ndarray, v: np.ndarray) -> np.ndarray
lag : callable
Lagrangian function.
Signature: lag(x: np.ndarray, mu: np.ndarray) -> float
lag_grad : callable
Gradient of the Lagrangian function.
Signature: lag_grad(x: np.ndarray, mu: np.ndarray) -> np.ndarray
grad_free : bool, default=False
If ``True``, ProblemLite will not use/generate any derivative information.
'''
self.check_types(x0, name, obj, con, grad, jac, obj_hess, lag_hess, fd_step, vp_fd_step,
xl, xu, cl, cu, x_scaler, o_scaler, c_scaler, lag, lag_grad, grad_free)
allowed_callbacks = ['obj', 'con', 'grad', 'jac', 'obj_hess', 'lag_hess', 'jvp', 'vjp', 'obj_hvp', 'lag_hvp', 'lag', 'lag_grad']
local_vars = locals()
self.user_defined_callbacks = [key for key in allowed_callbacks if local_vars[key] is not None]
self.problem_name = name
self.options = OptionsDictionary()
self.ny = 0
self.nx = nx = x0.size
self.fd_step = fd_step * np.ones((nx,))
fd_step = self.fd_step
self.vp_fd_step = vp_fd_step * 1.
self.o_scaler = o_scaler * np.ones((1,))
self.x_scaler = x_scaler * np.ones((nx,))
self.x0 = np.asarray(x0, dtype=float) * x_scaler
self.x_lower = xl * self.x_scaler if xl is not None else np.full(nx, -np.inf)
self.x_upper = xu * self.x_scaler if xu is not None else np.full(nx, np.inf)
self.jvp = jvp
self.vjp = vjp
self.obj_hvp = obj_hvp
self.lag_hvp = lag_hvp
if obj is not None:
self.obj = obj
else:
if any(cb is not None for cb in [grad, obj_hess, obj_hvp]):
raise ValueError('Objective function "obj" is not provided but at least one of '\
'"grad", "obj_hess", or "obj_hvp" is declared.')
print('Objective function not provided. Running a feasibility problem.')
self.obj = lambda x: 0.0
self.grad = lambda x: np.zeros((self.nx,))
self.obj_hess = lambda x: np.zeros((self.nx, self.nx))
self.constrained = False
self.nc = 0
self.con = con
if con is not None:
self.constrained = True
if grad is not None:
self.grad = grad
elif (obj is not None) and (not grad_free):
warnings.warn('Gradient function "grad" not provided. Finite differences will be used if gradient computation is necessary.')
# FD gradient
def fd_grad(x):
f0 = self.obj(x)
return np.array([(self.obj(x + fd_step*np.eye(self.nx)[:,i]) - f0) / fd_step[i] for i in range(self.nx)])
self.grad = fd_grad
self.jac = jac
if (con is not None) and (jac is None) and (not grad_free):
warnings.warn('Jacobian function "jac" not provided. Finite differences will be used if Jacobian computation is necessary.')
# FD Jacobian
def fd_jac(x):
c0 = self.con(x)
return np.array([(self.con(x + fd_step*np.eye(self.nx)[:,i]) - c0) / fd_step[i] for i in range(self.nx)]).T
self.jac = fd_jac
if self.constrained:
if lag is not None:
self.lag = lag
else:
self.lag = lambda x, mu: self.obj(x) + np.dot(mu, self.con(x))
if lag_grad is not None:
self.lag_grad = lag_grad
elif not grad_free:
self.lag_grad = lambda x, mu: self.grad(x) + np.dot(mu, self.jac(x))
else:
if any(cb is not None for cb in [jac, vjp, jvp, lag, lag_grad, lag_hess, lag_hvp]):
raise ValueError('Constraint function "con" is not provided but at least one of '\
'"jac", "vjp", "jvp", "lag", "lag_grad", "lag_hess", or "lag_hvp" is declared.')
if cl is not None or cu is not None or c_scaler != 1.0:
raise ValueError('If "con" function is not provided, "cl", "cu", and "c_scaler" must not be declared.')
if obj_hess is not None:
self.obj_hess = obj_hess
elif (obj is not None) and (not grad_free):
warnings.warn('Objective Hessian function "obj_hess" not provided. Finite differences will be used if objective Hessian computation is necessary.')
# FD Hessian
def fd_obj_hess(x):
g0 = self.grad(x)
return np.array([(self.grad(x + fd_step*np.eye(self.nx)[:,i]) - g0) / fd_step[i] for i in range(self.nx)])
self.obj_hess = fd_obj_hess
if con is not None:
if lag_hess is not None:
self.lag_hess = lag_hess
elif not grad_free:
warnings.warn('Lagrangian Hessian function "lag_hess" not provided. Finite differences will be used if Lagrangian Hessian computation is necessary.')
# FD Lagrangian Hessian
def fd_lag_hess(x, mu):
lg0 = self.lag_grad(x, mu)
return np.array([(self.lag_grad(x + fd_step*np.eye(self.nx)[:,i], mu) - lg0) / fd_step[i] for i in range(self.nx)])
self.lag_hess = fd_lag_hess
self.nfev = 0
self.ngev = 0
self.fev_time = 0.0
self.gev_time = 0.0
self.warm_x = self.x = np.random.rand(self.nx)
self.warm_x_derivs = np.random.rand(self.nx)
# self.warm_x_2nd_derivs = np.random.rand(self.nx) # No caching of 2nd derivatives since memory expensive
# Cached f, c, g, j are all scaled
self.f = None
self.g = None
self.c = None
self.j = None
# For the first call, we expand _funcs(x0) below to avoid redundant calls to con(x0)
# to get the size of the constraints nc and to perform checks on sizes of functions.
# self.c_scaler = c_scaler * 1.
# self._funcs(self.x0)
# if self.constrained:
# self.nc = nc = self.c.size
# NOTE: x0 is unscaled while self.x0 is scaled
###### FIRST RUN FOR OBJECTIVE AND CONSTRAINTS ######
f_start = time.time()
f0 = self.obj(x0)
if not np.isscalar(f0):
if f0.shape != (1,):
raise ValueError('Objective function "obj" must return a scalar or a 1D array with shape (1,).')
elif not np.isreal(f0):
raise ValueError('Objective function "obj" must return a real-valued scalar.')
self.f = (f0 * self.o_scaler)[0]
if self.constrained:
c0 = self.con(x0)
self.nc = nc = c0.size
if c0.shape != (nc,):
raise ValueError(f'Constraint function "con" must return a 1D array with shape (nc,) '
f'where nc={nc} is the number of constraints.')
self.c = c0 * c_scaler
self.warm_x[:] = self.x0
self.nfev += 1
self.fev_time += time.time() - f_start
#####################################################
# Once nc is known from the first call to con(x0), update the cl, cu, c_scaler, mu sizes
if self.constrained:
self.c_scaler = c_scaler * np.ones((nc,))
self.c_lower = cl * self.c_scaler if cl is not None else np.full(nc, -np.inf)
self.c_upper = cu * self.c_scaler if cu is not None else np.full(nc, np.inf)
self.warm_mu = self.mu = np.full((self.nc,), 0.) if self.constrained else None
else:
self.c_lower = self.c_upper = self.c_scaler = self.warm_mu = None
# For the first call, we expand _derivs(x0) here to perform checks on sizes of derivatives returned.
# self._derivs(self.x0)
###### FIRST RUN FOR GRADIENT AND JACOBIAN ######
if not grad_free:
g_start = time.time()
g0 = self.grad(x0)
wrong_grad = False
if np.isscalar(g0):
wrong_grad = True
elif g0.shape != (nx,):
wrong_grad = True
if wrong_grad:
raise ValueError(f'Gradient function "grad" must return a 1D array with shape (nx,), '
f'where nx={nx} is the number of design variables.')
self.g = g0 * self.o_scaler / self.x_scaler
if self.constrained:
j0 = self.jac(x0)
wrong_jac = False
if np.isscalar(j0):
wrong_jac = True
elif j0.shape != (nc, nx):
wrong_jac = True
if wrong_jac:
raise ValueError(f'Jacobian function "jac" must return a 2D array with shape (nc, nx), '
f'where nc={nc} is the number of constraints and nx={nx} is the number of design variables.')
self.j = j0 * np.outer(self.c_scaler, 1 / self.x_scaler)
self.warm_x_derivs[:] = self.x0
self.ngev += 1
self.gev_time += time.time() - g_start
#####################################################
self.check_shapes(x0, xl, xu, cl, cu, x_scaler, o_scaler, c_scaler)
# private attributes for recording, hot-starting, and visualization
self._record = None
self._callback_count = 0
self._obj_count = 0
self._grad_count = 0
self._hess_count = 0
self._con_count = 0
self._jac_count = 0
self._hot_start_mode = False
self._hot_start_record = None
self._num_callbacks_found = 0
self._hot_start_tol = None
self._reused_callback_count = 0
self._visualizer = None
def check_types(self, x0, name, obj, con, grad, jac, obj_hess, lag_hess, fd_step, vp_fd_step,
xl, xu, cl, cu, x_scaler, o_scaler, c_scaler, lag, lag_grad, grad_free):
if not isinstance(x0, np.ndarray):
raise TypeError('Initial guess x0 must be a numpy array.')
if not isinstance(name, str):
raise TypeError('Problem "name" must be a string.')
if not isinstance(grad_free, bool):
raise TypeError('"grad_free" argument must be a boolean.')
def check_callable(func_list, name_list):
for func, name in zip(func_list, name_list):
if func is not None and not callable(func):
raise TypeError(f'{name} must be a callable function.')
check_callable([obj, con, grad, jac, obj_hess, lag_hess, lag, lag_grad],
['Objective function "obj"', 'Constraint function "con"', 'Objective gradient "grad"',
'Constraint Jacobian "jac"', 'Objective Hessian "obj_hess"', 'Lagrangian Hessian "lag_hess"',
'Lagrangian "lag"', 'Lagrangian gradient "lag_grad"'])
def check_real_scalar(val_list, name_list):
for val, name in zip(val_list, name_list):
if not np.isscalar(val) or not np.isreal(val):
raise TypeError(f'{name} must be a real-valued scalar.')
check_real_scalar([vp_fd_step, o_scaler], ['Vector product finite difference step "vp_fd_step"',
'Objective scaler "o_scaler"', ])
def check_scalar_or_array(val_list, name_list):
for val, name in zip(val_list, name_list):
if (not np.isscalar(val) or not np.isreal(val)) and not isinstance(val, np.ndarray) and val is not None:
raise TypeError(f'{name} must be a real-valued scalar or a numpy array.')
check_scalar_or_array([fd_step, xl, xu, cl, cu, x_scaler, c_scaler],
['Finite difference step "fd_step"', 'Variable lower bounds "xl"', 'Variable upper bounds "xu"',
'Constraint lower bounds "cl"', 'Constraint upper bounds "cu"', 'Design variable scaler "x_scaler"',
'Constraint scaler "c_scaler"'])
def check_shapes(self, x0, xl, xu, cl, cu, x_scaler, o_scaler, c_scaler):
'''
Check for errors in the optimization problem setup.
'''
if self.nx == 0:
raise ValueError('No design variables declared. "x0" has size 0. Please provide a non-empty initial guess.')
if x0.shape != (self.nx,):
raise ValueError(f'The initial guess vector must be a 1D array but provided x0 has shape {x0.shape}.')
if self.x_lower.shape != (self.nx,):
raise ValueError(f'The lower bounds vector must be a real number or of shape ({self.nx},) but got shape {xl.shape}.')
if self.x_upper.shape != (self.nx,):
raise ValueError(f'The upper bounds vector must be a real number or of shape ({self.nx},) but got shape {xu.shape}.')
if self.x_scaler.shape != (self.nx,):
raise ValueError(f'The design variable scaler vector must be a real number or of shape ({self.nx},) but got shape {x_scaler.shape}.')
if not np.isscalar(o_scaler):
if o_scaler.shape != (1,):
raise ValueError(f'The objective scaler must be a scalar or a 1D array with shape (1,) but got shape {o_scaler.shape}.')
if self.constrained:
if self.nc == 0:
raise ValueError('Constraints are declared but the number of constraints is zero.')
if self.c_lower.shape != (self.nc,):
raise ValueError(f'Lower bounds vector for constraints must be a real number or of shape ({self.nc},) but got shape {cl.shape}.')
if self.c_upper.shape != (self.nc,):
raise ValueError(f'Upper bounds vector for constraints must be a real number or of shape ({self.nc},) but got shape {cu.shape}.')
if self.c_scaler.shape != (self.nc,):
raise ValueError(f'The constraint scaler vector must be a real number or of shape ({self.nc},) but got shape {c_scaler.shape}.')
def _funcs(self, x):
'''
Compute the objective and constraints at the given x, if x is different from the previous x.
'''
if not np.array_equal(x, self.warm_x):
f_start = time.time()
self.f = (self.obj(x/self.x_scaler) * self.o_scaler)[0]
if self.constrained:
self.c = self.con(x/self.x_scaler) * self.c_scaler
self.warm_x[:] = x
self.nfev += 1
self.fev_time += time.time() - f_start
def _derivs(self, x):
'''
Compute the gradient and Jacobian at the given x, if x is different from the previous x.
'''
if not np.array_equal(x, self.warm_x_derivs):
g_start = time.time()
self.g = self.grad(x/self.x_scaler) * self.o_scaler / self.x_scaler
if self.constrained:
self.j = self.jac(x/self.x_scaler) * np.outer(self.c_scaler, 1 / self.x_scaler)
self.warm_x_derivs[:] = x
self.ngev += 1
self.gev_time += time.time() - g_start
@record(['x'],['obj'])
@hot_start(['x'],['obj'])
def _compute_objective(self, x):
'''
Compute the (scaled) objective function at the given x.
'''
self._funcs(x)
return self.f
@record(['x'],['grad'])
@hot_start(['x'],['grad'])
def _compute_objective_gradient(self, x):
'''
Compute the (scaled) gradient of the objective function at the given x.
'''
self._derivs(x)
return self.g
@record(['x'],['con'])
@hot_start(['x'],['con'])
def _compute_constraints(self, x):
'''
Compute the (scaled) constraints at the given x.
'''
self._funcs(x)
return self.c
@record(['x'],['jac'])
@hot_start(['x'],['jac'])
def _compute_constraint_jacobian(self, x):
'''
Compute the (scaled) Jacobian of the constraints at the given x.
'''
self._derivs(x)
return self.j
@record(['x'],['obj_hess'])
@hot_start(['x'],['obj_hess'])
def _compute_objective_hessian(self, x):
'''
Compute the (scaled) Hessian of the objective at the given x.
'''
return self.obj_hess(x/self.x_scaler) * self.o_scaler * np.outer(1 / self.x_scaler, 1 / self.x_scaler)
@record(['x','mu'],['lag_hess'])
@hot_start(['x','mu'],['lag_hess'])
def _compute_lagrangian_hessian(self, x, mu):
'''
Compute the (scaled) Hessian of the Lagrangian at the given x and mu.
'''
self.warm_mu[:] = mu
return self.lag_hess(x/self.x_scaler, mu*self.c_scaler/self.o_scaler) * self.o_scaler * np.outer(1 / self.x_scaler, 1 / self.x_scaler)
@record(['x','mu'],['lag'])
@hot_start(['x','mu'],['lag'])
def _compute_lagrangian(self, x, mu):
'''
Compute the (scaled) Lagrangian at the given x and mu.
'''
# return self.lag(x, mu)
self._funcs(x)
self.warm_mu[:] = mu
return self.f + np.dot(mu, self.c)
@record(['x','mu'],['lag_grad'])
@hot_start(['x','mu'],['lag_grad'])
def _compute_lagrangian_gradient(self, x, mu):
'''
Compute the (scaled) gradient of the Lagrangian at the given x and mu.
'''
# return self.lag_grad(x, mu)
self._derivs(x)
self.warm_mu[:] = mu
return self.g + np.dot(mu, self.j)
@record(['x','v'],['jvp'])
@hot_start(['x','v'],['jvp'])
def _compute_constraint_jvp(self, x, v):
'''
Compute the (scaled) Jacobian-vector product at the given x and v.
'''
if self.jvp is not None:
return self.jvp(x/self.x_scaler, v/self.x_scaler) * self.c_scaler
else:
# warnings.warn("No constraint JVP function is declared. Computing full Jacobian to get JVP.")
# j = self.jac(x/self.x_scaler) * np.outer(self.c_scaler, 1 / self.x_scaler)
# return j @ v
warnings.warn("No constraint JVP function is declared. Using finite differences.")
c0 = self.con(x/self.x_scaler)
h = self.vp_fd_step * v / self.x_scaler
c1 = self.con(x/self.x_scaler + h)
fd_jvp = (c1 - c0) / self.vp_fd_step
return fd_jvp * self.c_scaler
@record(['x','v'],['vjp'])
@hot_start(['x','v'],['vjp'])
def _compute_constraint_vjp(self, x, v):
'''
Compute the (scaled) vector-Jacobian product at the given x and v.
'''
if self.vjp is not None:
return self.vjp(x/self.x_scaler, v*self.c_scaler) / self.x_scaler
else:
warnings.warn("No constraint VJP function is declared. Computing full Jacobian to get VJP.")
j = self.jac(x/self.x_scaler) * np.outer(self.c_scaler, 1 / self.x_scaler)
return v @ j
@record(['x','v'],['obj_hvp'])
@hot_start(['x','v'],['obj_hvp'])
def _compute_objective_hvp(self, x, v):
'''
Compute the (scaled) objective Hessian-vector product at the given x and v.
'''
if self.obj_hvp is not None:
return self.obj_hvp(x/self.x_scaler, v/self.x_scaler) * self.o_scaler / self.x_scaler
else:
# warnings.warn("No objective HVP function is declared. Computing full objective Hessian to get obj_HVP.")
# oh = self.obj_hess(x/self.x_scaler) * o_scaler * np.outer(1 / self.x_scaler, 1 / self.x_scaler)
# return oh @ v
warnings.warn("No objective HVP function is declared. Using finite differences.")
g0 = self.grad(x/self.x_scaler)
h = self.vp_fd_step * v / self.x_scaler
g1 = self.grad(x/self.x_scaler + h)
fd_hvp = (g1 - g0) / self.vp_fd_step
return fd_hvp * self.o_scaler / self.x_scaler
@record(['x','mu','v'],['lag_hvp'])
@hot_start(['x','mu','v'],['lag_hvp'])
def _compute_lagrangian_hvp(self, x, mu, v):
'''
Compute the (scaled) Lagrangian Hessian-vector product at the given x, v, and mu.
'''
self.warm_mu[:] = mu
if self.lag_hvp is not None:
return self.lag_hvp(x/self.x_scaler, mu*self.c_scaler/self.o_scaler, v/self.x_scaler) * self.o_scaler / self.x_scaler
else:
# warnings.warn("No Lagrangian HVP function is declared. Computing full lagrangian Hessian to get lag_HVP.")
# lh = self.lag_hess(x/self.x_scaler, mu*self.c_scaler/self.o_scaler) * self.o_scaler * np.outer(1 / self.x_scaler, 1 / self.x_scaler)
# return lh @ v
warnings.warn("No Lagrangian HVP function is declared. Using finite differences.")
lg0 = self.lag_grad(x/self.x_scaler, mu*self.c_scaler/self.o_scaler)
h = self.vp_fd_step * v / self.x_scaler
lg1 = self.lag_grad(x/self.x_scaler + h, mu*self.c_scaler/self.o_scaler)
fd_hvp = (lg1 - lg0) / self.vp_fd_step
return fd_hvp * self.o_scaler / self.x_scaler
def compute_objective(self, dvs, obj):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_objective_gradient(self, dvs, grad):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_constraints(self, dvs, con):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_constraint_jacobian(self, dvs, jac):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_lagrangian(self, dvs, lag_mult, lag):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_lagrangian_gradient(self, dvs, lag_mult, lag_grad):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_objective_hessian(self, dvs, obj_hess):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_lagrangian_hessian(self, dvs, lag_mult, lag_hess):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_constraint_jvp(self, dvs, vec, jvp):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_constraint_vjp(self, dvs, vec, vjp):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_objective_hvp(self, dvs, vec, obj_hvp):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
def compute_lagrangian_hvp(self, dvs, lag_mult, vec, lag_hvp):
raise NotImplementedError(COMPUTE_NOT_IMPLEMENTED_ERROR)
[docs] def __str__(self):
"""
Print the details of the UNSCALED optimization problem.
"""
name = self.problem_name
obj = (self.f / self.o_scaler)[0] # self.o_scaler always has shape (1,)
obj_scaler = self.o_scaler[0]
dvs = self.warm_x / self.x_scaler
x_l = self.x_lower / self.x_scaler
x_u = self.x_upper / self.x_scaler
x_s = self.x_scaler
if self.constrained:
cons = self.c / self.c_scaler
c_l = self.c_lower / self.c_scaler
c_u = self.c_upper / self.c_scaler
c_s = self.c_scaler
mu = self.warm_mu
# output = '\n\t'+'-'*100
output = f'\n\tProblem Overview:\n\t' + '-'*100
output += f'\n\t' + pad_name('Problem name', 25) + f': {name}'
output += f'\n\t' + pad_name('Objectives', 25) + f': obj'
output += f'\n\t' + pad_name('Design variables', 25) + f': x (shape: {dvs.shape})'
if self.constrained:
output += f'\n\t' + pad_name('Constraints', 25) + f': con (shape: {cons.shape})'
output += '\n\t' + '-'*100
output += f'\n\n\tProblem Data (UNSCALED):\n'
output += '\t' + '-'*100
# Print objective data
output += f'\n\tObjectives:\n'
header = "\t%-5s | %-10s | %-13s | %-13s " % ('Index', 'Name', 'Scaler', 'Value')
output += header
obj_template = "\n\t{idx:>5} | {name:<10} | {scaler:<+.6e} | {value:<+.6e}"
output += obj_template.format(idx=0, name='obj', scaler=obj_scaler, value=obj)
# Print design variable data
output += f'\n\n\tDesign Variables:\n'
header = "\t%-5s | %-10s | %-13s | %-13s | %-13s | %-13s " % ('Index', 'Name', 'Scaler', 'Lower Limit', 'Value', 'Upper Limit')
output += header
dv_template = "\n\t{idx:>5} | {name:<10} | {scaler:<+.6e} | {lower:<+.6e} | {value:<+.6e} | {upper:<+.6e}"
for i, x in enumerate(dvs.flatten()):
l = -1.e99 if x_l[i] == -np.inf else x_l[i]
u = +1.e99 if x_u[i] == +np.inf else x_u[i]
output += dv_template.format(idx=i, name='x'+f'[{i}]', scaler=x_s[i], lower=l, value=x, upper=u)
# Print constraint data
if self.constrained:
output += f'\n\n\tConstraints:\n'
header = "\t%-5s | %-10s | %-13s | %-13s | %-13s | %-13s | %-13s " % ('Index', 'Name', 'Scaler','Lower Limit', 'Value', 'Upper Limit', 'Lag. mult.')
output += header
zero_lag = np.array_equal(mu, np.zeros((self.nc,)))
if not zero_lag: # print lagrange multipliers
con_template = "\n\t{idx:>5} | {name:<10} | {scaler:<+.6e} | {lower:<+.6e} | {value:<+.6e} | {upper:<+.6e} | {lag:<+.6e} "
else:
con_template = "\n\t{idx:>5} | {name:<10} | {scaler:<+.6e} | {lower:<+.6e} | {value:<+.6e} | {upper:<+.6e} | "
for i, c in enumerate(cons.flatten()):
l = -1.e99 if c_l[i] == -np.inf else c_l[i]
u = +1.e99 if c_u[i] == +np.inf else c_u[i]
if not zero_lag: # print lagrange multipliers
output += con_template.format(idx=i, name='con'+f'[{i}]', scaler=c_s[i], lower=l, value=c, upper=u, lag=mu[i])
else:
output += con_template.format(idx=i, name='con'+f'[{i}]', scaler=c_s[i], lower=l, value=c, upper=u)
output += '\n\t' + '-'*100 + '\n'
return output