2024-10-02 22:15:59 +04:00

1288 lines
39 KiB
Python

from contextlib import suppress
from inspect import signature
import copy
import numpy as np
from scipy.optimize import (
Bounds,
LinearConstraint,
NonlinearConstraint,
OptimizeResult,
)
from scipy.optimize._constraints import PreparedConstraint
from .settings import PRINT_OPTIONS, BARRIER
from .utils import CallbackSuccess, get_arrays_tol
from .utils import exact_1d_array
class ObjectiveFunction:
"""
Real-valued objective function.
"""
def __init__(self, fun, verbose, debug, *args):
"""
Initialize the objective function.
Parameters
----------
fun : {callable, None}
Function to evaluate, or None.
``fun(x, *args) -> float``
where ``x`` is an array with shape (n,) and `args` is a tuple.
verbose : bool
Whether to print the function evaluations.
debug : bool
Whether to make debugging tests during the execution.
*args : tuple
Additional arguments to be passed to the function.
"""
if debug:
assert fun is None or callable(fun)
assert isinstance(verbose, bool)
assert isinstance(debug, bool)
self._fun = fun
self._verbose = verbose
self._args = args
self._n_eval = 0
def __call__(self, x):
"""
Evaluate the objective function.
Parameters
----------
x : array_like, shape (n,)
Point at which the objective function is evaluated.
Returns
-------
float
Function value at `x`.
"""
x = np.array(x, dtype=float)
if self._fun is None:
f = 0.0
else:
f = float(np.squeeze(self._fun(x, *self._args)))
self._n_eval += 1
if self._verbose:
with np.printoptions(**PRINT_OPTIONS):
print(f"{self.name}({x}) = {f}")
return f
@property
def n_eval(self):
"""
Number of function evaluations.
Returns
-------
int
Number of function evaluations.
"""
return self._n_eval
@property
def name(self):
"""
Name of the objective function.
Returns
-------
str
Name of the objective function.
"""
name = ""
if self._fun is not None:
try:
name = self._fun.__name__
except AttributeError:
name = "fun"
return name
class BoundConstraints:
"""
Bound constraints ``xl <= x <= xu``.
"""
def __init__(self, bounds):
"""
Initialize the bound constraints.
Parameters
----------
bounds : scipy.optimize.Bounds
Bound constraints.
"""
self._xl = np.array(bounds.lb, float)
self._xu = np.array(bounds.ub, float)
# Remove the ill-defined bounds.
self.xl[np.isnan(self.xl)] = -np.inf
self.xu[np.isnan(self.xu)] = np.inf
self.is_feasible = (
np.all(self.xl <= self.xu)
and np.all(self.xl < np.inf)
and np.all(self.xu > -np.inf)
)
self.m = np.count_nonzero(self.xl > -np.inf) + np.count_nonzero(
self.xu < np.inf
)
self.pcs = PreparedConstraint(bounds, np.ones(bounds.lb.size))
@property
def xl(self):
"""
Lower bound.
Returns
-------
`numpy.ndarray`, shape (n,)
Lower bound.
"""
return self._xl
@property
def xu(self):
"""
Upper bound.
Returns
-------
`numpy.ndarray`, shape (n,)
Upper bound.
"""
return self._xu
def maxcv(self, x):
"""
Evaluate the maximum constraint violation.
Parameters
----------
x : array_like, shape (n,)
Point at which the maximum constraint violation is evaluated.
Returns
-------
float
Maximum constraint violation at `x`.
"""
x = np.asarray(x, dtype=float)
return self.violation(x)
def violation(self, x):
# shortcut for no bounds
if self.is_feasible:
return np.array([0])
else:
return self.pcs.violation(x)
def project(self, x):
"""
Project a point onto the feasible set.
Parameters
----------
x : array_like, shape (n,)
Point to be projected.
Returns
-------
`numpy.ndarray`, shape (n,)
Projection of `x` onto the feasible set.
"""
return np.clip(x, self.xl, self.xu) if self.is_feasible else x
class LinearConstraints:
"""
Linear constraints ``a_ub @ x <= b_ub`` and ``a_eq @ x == b_eq``.
"""
def __init__(self, constraints, n, debug):
"""
Initialize the linear constraints.
Parameters
----------
constraints : list of LinearConstraint
Linear constraints.
n : int
Number of variables.
debug : bool
Whether to make debugging tests during the execution.
"""
if debug:
assert isinstance(constraints, list)
for constraint in constraints:
assert isinstance(constraint, LinearConstraint)
assert isinstance(debug, bool)
self._a_ub = np.empty((0, n))
self._b_ub = np.empty(0)
self._a_eq = np.empty((0, n))
self._b_eq = np.empty(0)
for constraint in constraints:
is_equality = np.abs(
constraint.ub - constraint.lb
) <= get_arrays_tol(constraint.lb, constraint.ub)
if np.any(is_equality):
self._a_eq = np.vstack((self.a_eq, constraint.A[is_equality]))
self._b_eq = np.concatenate(
(
self.b_eq,
0.5
* (
constraint.lb[is_equality]
+ constraint.ub[is_equality]
),
)
)
if not np.all(is_equality):
self._a_ub = np.vstack(
(
self.a_ub,
constraint.A[~is_equality],
-constraint.A[~is_equality],
)
)
self._b_ub = np.concatenate(
(
self.b_ub,
constraint.ub[~is_equality],
-constraint.lb[~is_equality],
)
)
# Remove the ill-defined constraints.
self.a_ub[np.isnan(self.a_ub)] = 0.0
self.a_eq[np.isnan(self.a_eq)] = 0.0
undef_ub = np.isnan(self.b_ub) | np.isinf(self.b_ub)
undef_eq = np.isnan(self.b_eq)
self._a_ub = self.a_ub[~undef_ub, :]
self._b_ub = self.b_ub[~undef_ub]
self._a_eq = self.a_eq[~undef_eq, :]
self._b_eq = self.b_eq[~undef_eq]
self.pcs = [
PreparedConstraint(c, np.ones(n)) for c in constraints if c.A.size
]
@property
def a_ub(self):
"""
Left-hand side matrix of the linear inequality constraints.
Returns
-------
`numpy.ndarray`, shape (m, n)
Left-hand side matrix of the linear inequality constraints.
"""
return self._a_ub
@property
def b_ub(self):
"""
Right-hand side vector of the linear inequality constraints.
Returns
-------
`numpy.ndarray`, shape (m, n)
Right-hand side vector of the linear inequality constraints.
"""
return self._b_ub
@property
def a_eq(self):
"""
Left-hand side matrix of the linear equality constraints.
Returns
-------
`numpy.ndarray`, shape (m, n)
Left-hand side matrix of the linear equality constraints.
"""
return self._a_eq
@property
def b_eq(self):
"""
Right-hand side vector of the linear equality constraints.
Returns
-------
`numpy.ndarray`, shape (m, n)
Right-hand side vector of the linear equality constraints.
"""
return self._b_eq
@property
def m_ub(self):
"""
Number of linear inequality constraints.
Returns
-------
int
Number of linear inequality constraints.
"""
return self.b_ub.size
@property
def m_eq(self):
"""
Number of linear equality constraints.
Returns
-------
int
Number of linear equality constraints.
"""
return self.b_eq.size
def maxcv(self, x):
"""
Evaluate the maximum constraint violation.
Parameters
----------
x : array_like, shape (n,)
Point at which the maximum constraint violation is evaluated.
Returns
-------
float
Maximum constraint violation at `x`.
"""
return np.max(self.violation(x), initial=0.0)
def violation(self, x):
if len(self.pcs):
return np.concatenate([pc.violation(x) for pc in self.pcs])
return np.array([])
class NonlinearConstraints:
"""
Nonlinear constraints ``c_ub(x) <= 0`` and ``c_eq(x) == b_eq``.
"""
def __init__(self, constraints, verbose, debug):
"""
Initialize the nonlinear constraints.
Parameters
----------
constraints : list
Nonlinear constraints.
verbose : bool
Whether to print the function evaluations.
debug : bool
Whether to make debugging tests during the execution.
"""
if debug:
assert isinstance(constraints, list)
for constraint in constraints:
assert isinstance(constraint, NonlinearConstraint)
assert isinstance(verbose, bool)
assert isinstance(debug, bool)
self._constraints = constraints
self.pcs = []
self._verbose = verbose
# map of indexes for equality and inequality constraints
self._map_ub = None
self._map_eq = None
self._m_ub = self._m_eq = None
def __call__(self, x):
"""
Calculates the residual (slack) for the constraints.
Parameters
----------
x : array_like, shape (n,)
Point at which the constraints are evaluated.
Returns
-------
`numpy.ndarray`, shape (m_nonlinear_ub,)
Nonlinear inequality constraint slack values.
`numpy.ndarray`, shape (m_nonlinear_eq,)
Nonlinear equality constraint slack values.
"""
if not len(self._constraints):
self._m_eq = self._m_ub = 0
return np.array([]), np.array([])
x = np.array(x, dtype=float)
# first time around the constraints haven't been prepared
if not len(self.pcs):
self._map_ub = []
self._map_eq = []
self._m_eq = 0
self._m_ub = 0
for constraint in self._constraints:
if not callable(constraint.jac):
# having a callable constraint function prevents
# constraint.fun from being evaluated when preparing
# constraint
c = copy.copy(constraint)
c.jac = lambda x0: x0
c.hess = lambda x0, v: 0.0
pc = PreparedConstraint(c, x)
else:
pc = PreparedConstraint(constraint, x)
# we're going to be using the same x value again immediately
# after this initialisation
pc.fun.f_updated = True
self.pcs.append(pc)
idx = np.arange(pc.fun.m)
# figure out equality and inequality maps
lb, ub = pc.bounds[0], pc.bounds[1]
arr_tol = get_arrays_tol(lb, ub)
is_equality = np.abs(ub - lb) <= arr_tol
self._map_eq.append(idx[is_equality])
self._map_ub.append(idx[~is_equality])
# these values will be corrected to their proper values later
self._m_eq += np.count_nonzero(is_equality)
self._m_ub += np.count_nonzero(~is_equality)
c_ub = []
c_eq = []
for i, pc in enumerate(self.pcs):
val = pc.fun.fun(x)
if self._verbose:
with np.printoptions(**PRINT_OPTIONS):
with suppress(AttributeError):
fun_name = self._constraints[i].fun.__name__
print(f"{fun_name}({x}) = {val}")
# separate violations into c_eq and c_ub
eq_idx = self._map_eq[i]
ub_idx = self._map_ub[i]
ub_val = val[ub_idx]
if len(ub_idx):
xl = pc.bounds[0][ub_idx]
xu = pc.bounds[1][ub_idx]
# calculate slack within lower bound
finite_xl = xl > -np.inf
_v = xl[finite_xl] - ub_val[finite_xl]
c_ub.append(_v)
# calculate slack within lower bound
finite_xu = xu < np.inf
_v = ub_val[finite_xu] - xu[finite_xu]
c_ub.append(_v)
# equality constraints taken from midpoint between lb and ub
eq_val = val[eq_idx]
if len(eq_idx):
midpoint = 0.5 * (pc.bounds[1][eq_idx] + pc.bounds[0][eq_idx])
eq_val -= midpoint
c_eq.append(eq_val)
if self._m_eq:
c_eq = np.concatenate(c_eq)
else:
c_eq = np.array([])
if self._m_ub:
c_ub = np.concatenate(c_ub)
else:
c_ub = np.array([])
self._m_ub = c_ub.size
self._m_eq = c_eq.size
return c_ub, c_eq
@property
def m_ub(self):
"""
Number of nonlinear inequality constraints.
Returns
-------
int
Number of nonlinear inequality constraints.
Raises
------
ValueError
If the number of nonlinear inequality constraints is unknown.
"""
if self._m_ub is None:
raise ValueError(
"The number of nonlinear inequality constraints is unknown."
)
else:
return self._m_ub
@property
def m_eq(self):
"""
Number of nonlinear equality constraints.
Returns
-------
int
Number of nonlinear equality constraints.
Raises
------
ValueError
If the number of nonlinear equality constraints is unknown.
"""
if self._m_eq is None:
raise ValueError(
"The number of nonlinear equality constraints is unknown."
)
else:
return self._m_eq
@property
def n_eval(self):
"""
Number of function evaluations.
Returns
-------
int
Number of function evaluations.
"""
if len(self.pcs):
return self.pcs[0].fun.nfev
else:
return 0
def maxcv(self, x, cub_val=None, ceq_val=None):
"""
Evaluate the maximum constraint violation.
Parameters
----------
x : array_like, shape (n,)
Point at which the maximum constraint violation is evaluated.
cub_val : array_like, shape (m_nonlinear_ub,), optional
Values of the nonlinear inequality constraints. If not provided,
the nonlinear inequality constraints are evaluated at `x`.
ceq_val : array_like, shape (m_nonlinear_eq,), optional
Values of the nonlinear equality constraints. If not provided,
the nonlinear equality constraints are evaluated at `x`.
Returns
-------
float
Maximum constraint violation at `x`.
"""
return np.max(
self.violation(x, cub_val=cub_val, ceq_val=ceq_val), initial=0.0
)
def violation(self, x, cub_val=None, ceq_val=None):
return np.concatenate([pc.violation(x) for pc in self.pcs])
class Problem:
"""
Optimization problem.
"""
def __init__(
self,
obj,
x0,
bounds,
linear,
nonlinear,
callback,
feasibility_tol,
scale,
store_history,
history_size,
filter_size,
debug,
):
"""
Initialize the nonlinear problem.
The problem is preprocessed to remove all the variables that are fixed
by the bound constraints.
Parameters
----------
obj : ObjectiveFunction
Objective function.
x0 : array_like, shape (n,)
Initial guess.
bounds : BoundConstraints
Bound constraints.
linear : LinearConstraints
Linear constraints.
nonlinear : NonlinearConstraints
Nonlinear constraints.
callback : {callable, None}
Callback function.
feasibility_tol : float
Tolerance on the constraint violation.
scale : bool
Whether to scale the problem according to the bounds.
store_history : bool
Whether to store the function evaluations.
history_size : int
Maximum number of function evaluations to store.
filter_size : int
Maximum number of points in the filter.
debug : bool
Whether to make debugging tests during the execution.
"""
if debug:
assert isinstance(obj, ObjectiveFunction)
assert isinstance(bounds, BoundConstraints)
assert isinstance(linear, LinearConstraints)
assert isinstance(nonlinear, NonlinearConstraints)
assert isinstance(feasibility_tol, float)
assert isinstance(scale, bool)
assert isinstance(store_history, bool)
assert isinstance(history_size, int)
if store_history:
assert history_size > 0
assert isinstance(filter_size, int)
assert filter_size > 0
assert isinstance(debug, bool)
self._obj = obj
self._linear = linear
self._nonlinear = nonlinear
if callback is not None:
if not callable(callback):
raise TypeError("The callback must be a callable function.")
self._callback = callback
# Check the consistency of the problem.
x0 = exact_1d_array(x0, "The initial guess must be a vector.")
n = x0.size
if bounds.xl.size != n:
raise ValueError(f"The bounds must have {n} elements.")
if linear.a_ub.shape[1] != n:
raise ValueError(
f"The left-hand side matrices of the linear constraints must "
f"have {n} columns."
)
# Check which variables are fixed.
tol = get_arrays_tol(bounds.xl, bounds.xu)
self._fixed_idx = (bounds.xl <= bounds.xu) & (
np.abs(bounds.xl - bounds.xu) < tol
)
self._fixed_val = 0.5 * (
bounds.xl[self._fixed_idx] + bounds.xu[self._fixed_idx]
)
self._fixed_val = np.clip(
self._fixed_val,
bounds.xl[self._fixed_idx],
bounds.xu[self._fixed_idx],
)
# Set the bound constraints.
self._orig_bounds = bounds
self._bounds = BoundConstraints(
Bounds(bounds.xl[~self._fixed_idx], bounds.xu[~self._fixed_idx])
)
# Set the initial guess.
self._x0 = self._bounds.project(x0[~self._fixed_idx])
# Set the linear constraints.
b_eq = linear.b_eq - linear.a_eq[:, self._fixed_idx] @ self._fixed_val
self._linear = LinearConstraints(
[
LinearConstraint(
linear.a_ub[:, ~self._fixed_idx],
-np.inf,
linear.b_ub
- linear.a_ub[:, self._fixed_idx] @ self._fixed_val,
),
LinearConstraint(linear.a_eq[:, ~self._fixed_idx], b_eq, b_eq),
],
self.n,
debug,
)
# Scale the problem if necessary.
scale = (
scale
and self._bounds.is_feasible
and np.all(np.isfinite(self._bounds.xl))
and np.all(np.isfinite(self._bounds.xu))
)
if scale:
self._scaling_factor = 0.5 * (self._bounds.xu - self._bounds.xl)
self._scaling_shift = 0.5 * (self._bounds.xu + self._bounds.xl)
self._bounds = BoundConstraints(
Bounds(-np.ones(self.n), np.ones(self.n))
)
b_eq = self._linear.b_eq - self._linear.a_eq @ self._scaling_shift
self._linear = LinearConstraints(
[
LinearConstraint(
self._linear.a_ub @ np.diag(self._scaling_factor),
-np.inf,
self._linear.b_ub
- self._linear.a_ub @ self._scaling_shift,
),
LinearConstraint(
self._linear.a_eq @ np.diag(self._scaling_factor),
b_eq,
b_eq,
),
],
self.n,
debug,
)
self._x0 = (self._x0 - self._scaling_shift) / self._scaling_factor
else:
self._scaling_factor = np.ones(self.n)
self._scaling_shift = np.zeros(self.n)
# Set the initial filter.
self._feasibility_tol = feasibility_tol
self._filter_size = filter_size
self._fun_filter = []
self._maxcv_filter = []
self._x_filter = []
# Set the initial history.
self._store_history = store_history
self._history_size = history_size
self._fun_history = []
self._maxcv_history = []
self._x_history = []
def __call__(self, x):
"""
Evaluate the objective and nonlinear constraint functions.
Parameters
----------
x : array_like, shape (n,)
Point at which the functions are evaluated.
Returns
-------
float
Objective function value.
`numpy.ndarray`, shape (m_nonlinear_ub,)
Nonlinear inequality constraint function values.
`numpy.ndarray`, shape (m_nonlinear_eq,)
Nonlinear equality constraint function values.
Raises
------
`cobyqa.utils.CallbackSuccess`
If the callback function raises a ``StopIteration``.
"""
# Evaluate the objective and nonlinear constraint functions.
x = np.asarray(x, dtype=float)
x_full = self.build_x(x)
fun_val = self._obj(x_full)
cub_val, ceq_val = self._nonlinear(x_full)
maxcv_val = self.maxcv(x, cub_val, ceq_val)
if self._store_history:
self._fun_history.append(fun_val)
self._maxcv_history.append(maxcv_val)
self._x_history.append(x)
if len(self._fun_history) > self._history_size:
self._fun_history.pop(0)
self._maxcv_history.pop(0)
self._x_history.pop(0)
# Add the point to the filter if it is not dominated by any point.
if np.isnan(fun_val) and np.isnan(maxcv_val):
include_point = len(self._fun_filter) == 0
elif np.isnan(fun_val):
include_point = all(
np.isnan(fun_filter)
and maxcv_val < maxcv_filter
or np.isnan(maxcv_filter)
for fun_filter, maxcv_filter in zip(
self._fun_filter,
self._maxcv_filter,
)
)
elif np.isnan(maxcv_val):
include_point = all(
np.isnan(maxcv_filter)
and fun_val < fun_filter
or np.isnan(fun_filter)
for fun_filter, maxcv_filter in zip(
self._fun_filter,
self._maxcv_filter,
)
)
else:
include_point = all(
fun_val < fun_filter or maxcv_val < maxcv_filter
for fun_filter, maxcv_filter in zip(
self._fun_filter,
self._maxcv_filter,
)
)
if include_point:
self._fun_filter.append(fun_val)
self._maxcv_filter.append(maxcv_val)
self._x_filter.append(x)
# Remove the points in the filter that are dominated by the new
# point. We must iterate in reverse order to avoid problems when
# removing elements from the list.
for k in range(len(self._fun_filter) - 2, -1, -1):
if np.isnan(fun_val):
remove_point = np.isnan(self._fun_filter[k])
elif np.isnan(maxcv_val):
remove_point = np.isnan(self._maxcv_filter[k])
else:
remove_point = (
np.isnan(self._fun_filter[k])
or np.isnan(self._maxcv_filter[k])
or fun_val <= self._fun_filter[k]
and maxcv_val <= self._maxcv_filter[k]
)
if remove_point:
self._fun_filter.pop(k)
self._maxcv_filter.pop(k)
self._x_filter.pop(k)
# Keep only the most recent points in the filter.
if len(self._fun_filter) > self._filter_size:
self._fun_filter.pop(0)
self._maxcv_filter.pop(0)
self._x_filter.pop(0)
# Evaluate the callback function after updating the filter to ensure
# that the current point can be returned by the method.
if self._callback is not None:
sig = signature(self._callback)
try:
if set(sig.parameters) == {"intermediate_result"}:
intermediate_result = OptimizeResult(x=x_full, fun=fun_val)
self._callback(intermediate_result=intermediate_result)
else:
self._callback(x_full)
except StopIteration as exc:
raise CallbackSuccess from exc
# Apply the extreme barriers and return.
if np.isnan(fun_val):
fun_val = BARRIER
cub_val[np.isnan(cub_val)] = BARRIER
ceq_val[np.isnan(ceq_val)] = BARRIER
fun_val = max(min(fun_val, BARRIER), -BARRIER)
cub_val = np.maximum(np.minimum(cub_val, BARRIER), -BARRIER)
ceq_val = np.maximum(np.minimum(ceq_val, BARRIER), -BARRIER)
return fun_val, cub_val, ceq_val
@property
def n(self):
"""
Number of variables.
Returns
-------
int
Number of variables.
"""
return self.x0.size
@property
def n_orig(self):
"""
Number of variables in the original problem (with fixed variables).
Returns
-------
int
Number of variables in the original problem (with fixed variables).
"""
return self._fixed_idx.size
@property
def x0(self):
"""
Initial guess.
Returns
-------
`numpy.ndarray`, shape (n,)
Initial guess.
"""
return self._x0
@property
def n_eval(self):
"""
Number of function evaluations.
Returns
-------
int
Number of function evaluations.
"""
return self._obj.n_eval
@property
def fun_name(self):
"""
Name of the objective function.
Returns
-------
str
Name of the objective function.
"""
return self._obj.name
@property
def bounds(self):
"""
Bound constraints.
Returns
-------
BoundConstraints
Bound constraints.
"""
return self._bounds
@property
def linear(self):
"""
Linear constraints.
Returns
-------
LinearConstraints
Linear constraints.
"""
return self._linear
@property
def m_bounds(self):
"""
Number of bound constraints.
Returns
-------
int
Number of bound constraints.
"""
return self.bounds.m
@property
def m_linear_ub(self):
"""
Number of linear inequality constraints.
Returns
-------
int
Number of linear inequality constraints.
"""
return self.linear.m_ub
@property
def m_linear_eq(self):
"""
Number of linear equality constraints.
Returns
-------
int
Number of linear equality constraints.
"""
return self.linear.m_eq
@property
def m_nonlinear_ub(self):
"""
Number of nonlinear inequality constraints.
Returns
-------
int
Number of nonlinear inequality constraints.
Raises
------
ValueError
If the number of nonlinear inequality constraints is not known.
"""
return self._nonlinear.m_ub
@property
def m_nonlinear_eq(self):
"""
Number of nonlinear equality constraints.
Returns
-------
int
Number of nonlinear equality constraints.
Raises
------
ValueError
If the number of nonlinear equality constraints is not known.
"""
return self._nonlinear.m_eq
@property
def fun_history(self):
"""
History of objective function evaluations.
Returns
-------
`numpy.ndarray`, shape (n_eval,)
History of objective function evaluations.
"""
return np.array(self._fun_history, dtype=float)
@property
def maxcv_history(self):
"""
History of maximum constraint violations.
Returns
-------
`numpy.ndarray`, shape (n_eval,)
History of maximum constraint violations.
"""
return np.array(self._maxcv_history, dtype=float)
@property
def type(self):
"""
Type of the problem.
The problem can be either 'unconstrained', 'bound-constrained',
'linearly constrained', or 'nonlinearly constrained'.
Returns
-------
str
Type of the problem.
"""
try:
if self.m_nonlinear_ub > 0 or self.m_nonlinear_eq > 0:
return "nonlinearly constrained"
elif self.m_linear_ub > 0 or self.m_linear_eq > 0:
return "linearly constrained"
elif self.m_bounds > 0:
return "bound-constrained"
else:
return "unconstrained"
except ValueError:
# The number of nonlinear constraints is not known. It may be zero
# if the user provided a nonlinear inequality and/or equality
# constraint function that returns an empty array. However, as this
# is not known before the first call to the function, we assume
# that the problem is nonlinearly constrained.
return "nonlinearly constrained"
@property
def is_feasibility(self):
"""
Whether the problem is a feasibility problem.
Returns
-------
bool
Whether the problem is a feasibility problem.
"""
return self.fun_name == ""
def build_x(self, x):
"""
Build the full vector of variables from the reduced vector.
Parameters
----------
x : array_like, shape (n,)
Reduced vector of variables.
Returns
-------
`numpy.ndarray`, shape (n_orig,)
Full vector of variables.
"""
x_full = np.empty(self.n_orig)
x_full[self._fixed_idx] = self._fixed_val
x_full[~self._fixed_idx] = (x * self._scaling_factor
+ self._scaling_shift)
return self._orig_bounds.project(x_full)
def maxcv(self, x, cub_val=None, ceq_val=None):
"""
Evaluate the maximum constraint violation.
Parameters
----------
x : array_like, shape (n,)
Point at which the maximum constraint violation is evaluated.
cub_val : array_like, shape (m_nonlinear_ub,), optional
Values of the nonlinear inequality constraints. If not provided,
the nonlinear inequality constraints are evaluated at `x`.
ceq_val : array_like, shape (m_nonlinear_eq,), optional
Values of the nonlinear equality constraints. If not provided,
the nonlinear equality constraints are evaluated at `x`.
Returns
-------
float
Maximum constraint violation at `x`.
"""
violation = self.violation(x, cub_val=cub_val, ceq_val=ceq_val)
if np.count_nonzero(violation):
return np.max(violation, initial=0.0)
else:
return 0.0
def violation(self, x, cub_val=None, ceq_val=None):
violation = []
if not self.bounds.is_feasible:
b = self.bounds.violation(x)
violation.append(b)
if len(self.linear.pcs):
lc = self.linear.violation(x)
violation.append(lc)
if len(self._nonlinear.pcs):
nlc = self._nonlinear.violation(x, cub_val, ceq_val)
violation.append(nlc)
if len(violation):
return np.concatenate(violation)
def best_eval(self, penalty):
"""
Return the best point in the filter and the corresponding objective and
nonlinear constraint function evaluations.
Parameters
----------
penalty : float
Penalty parameter
Returns
-------
`numpy.ndarray`, shape (n,)
Best point.
float
Corresponding objective function value.
float
Corresponding maximum constraint violation.
"""
# If the filter is empty, i.e., if no function evaluation has been
# performed, we evaluate the objective and nonlinear constraint
# functions at the initial guess.
if len(self._fun_filter) == 0:
self(self.x0)
# Find the best point in the filter.
fun_filter = np.array(self._fun_filter)
maxcv_filter = np.array(self._maxcv_filter)
x_filter = np.array(self._x_filter)
finite_idx = np.isfinite(maxcv_filter)
if np.any(finite_idx):
# At least one point has a finite maximum constraint violation.
feasible_idx = maxcv_filter <= self._feasibility_tol
if np.any(feasible_idx) and not np.all(
np.isnan(fun_filter[feasible_idx])
):
# At least one point is feasible and has a well-defined
# objective function value. We select the point with the least
# objective function value. If there is a tie, we select the
# point with the least maximum constraint violation. If there
# is still a tie, we select the most recent point.
fun_min_idx = feasible_idx & (
fun_filter <= np.nanmin(fun_filter[feasible_idx])
)
if np.count_nonzero(fun_min_idx) > 1:
fun_min_idx &= maxcv_filter <= np.min(
maxcv_filter[fun_min_idx]
)
i = np.flatnonzero(fun_min_idx)[-1]
elif np.any(feasible_idx):
# At least one point is feasible but no feasible point has a
# well-defined objective function value. We select the most
# recent feasible point.
i = np.flatnonzero(feasible_idx)[-1]
else:
# No point is feasible. We first compute the merit function
# value for each point.
merit_filter = np.full_like(fun_filter, np.nan)
merit_filter[finite_idx] = (
fun_filter[finite_idx] + penalty * maxcv_filter[finite_idx]
)
if np.all(np.isnan(merit_filter)):
# No point has a well-defined merit function value. In
# other words, among the points with a well-defined maximum
# constraint violation, none has a well-defined objective
# function value. We select the point with the least
# maximum constraint violation. If there is a tie, we
# select the most recent point.
min_maxcv_idx = maxcv_filter <= np.nanmin(maxcv_filter)
i = np.flatnonzero(min_maxcv_idx)[-1]
else:
# At least one point has a well-defined merit function
# value. We select the point with the least merit function
# value. If there is a tie, we select the point with the
# least maximum constraint violation. If there is still a
# tie, we select the point with the least objective
# function value. If there is still a tie, we select the
# most recent point.
merit_min_idx = merit_filter <= np.nanmin(merit_filter)
if np.count_nonzero(merit_min_idx) > 1:
merit_min_idx &= maxcv_filter <= np.min(
maxcv_filter[merit_min_idx]
)
if np.count_nonzero(merit_min_idx) > 1:
merit_min_idx &= fun_filter <= np.min(
fun_filter[merit_min_idx]
)
i = np.flatnonzero(merit_min_idx)[-1]
elif not np.all(np.isnan(fun_filter)):
# No maximum constraint violation is well-defined but at least one
# point has a well-defined objective function value. We select the
# point with the least objective function value. If there is a tie,
# we select the most recent point.
fun_min_idx = fun_filter <= np.nanmin(fun_filter)
i = np.flatnonzero(fun_min_idx)[-1]
else:
# No point has a well-defined maximum constraint violation or
# objective function value. We select the most recent point.
i = len(fun_filter) - 1
return (
self.bounds.project(x_filter[i, :]),
fun_filter[i],
maxcv_filter[i],
)