AIM-PIbd-32-Kurbanova-A-A/aimenv/Lib/site-packages/scipy/_lib/cobyqa/models.py

1526 lines
49 KiB
Python
Raw Normal View History

2024-10-02 22:15:59 +04:00
import warnings
import numpy as np
from scipy.linalg import eigh
from .settings import Options
from .utils import MaxEvalError, TargetSuccess, FeasibleSuccess
EPS = np.finfo(float).eps
class Interpolation:
"""
Interpolation set.
This class stores a base point around which the models are expanded and the
interpolation points. The coordinates of the interpolation points are
relative to the base point.
"""
def __init__(self, pb, options):
"""
Initialize the interpolation set.
Parameters
----------
pb : `cobyqa.problem.Problem`
Problem to be solved.
options : dict
Options of the solver.
"""
# Reduce the initial trust-region radius if necessary.
self._debug = options[Options.DEBUG]
max_radius = 0.5 * np.min(pb.bounds.xu - pb.bounds.xl)
if options[Options.RHOBEG] > max_radius:
options[Options.RHOBEG.value] = max_radius
options[Options.RHOEND.value] = np.min(
[
options[Options.RHOEND],
max_radius,
]
)
# Set the initial point around which the models are expanded.
self._x_base = np.copy(pb.x0)
very_close_xl_idx = (
self.x_base <= pb.bounds.xl + 0.5 * options[Options.RHOBEG]
)
self.x_base[very_close_xl_idx] = pb.bounds.xl[very_close_xl_idx]
close_xl_idx = (
pb.bounds.xl + 0.5 * options[Options.RHOBEG] < self.x_base
) & (self.x_base <= pb.bounds.xl + options[Options.RHOBEG])
self.x_base[close_xl_idx] = np.minimum(
pb.bounds.xl[close_xl_idx] + options[Options.RHOBEG],
pb.bounds.xu[close_xl_idx],
)
very_close_xu_idx = (
self.x_base >= pb.bounds.xu - 0.5 * options[Options.RHOBEG]
)
self.x_base[very_close_xu_idx] = pb.bounds.xu[very_close_xu_idx]
close_xu_idx = (
self.x_base < pb.bounds.xu - 0.5 * options[Options.RHOBEG]
) & (pb.bounds.xu - options[Options.RHOBEG] <= self.x_base)
self.x_base[close_xu_idx] = np.maximum(
pb.bounds.xu[close_xu_idx] - options[Options.RHOBEG],
pb.bounds.xl[close_xu_idx],
)
# Set the initial interpolation set.
self._xpt = np.zeros((pb.n, options[Options.NPT]))
for k in range(1, options[Options.NPT]):
if k <= pb.n:
if very_close_xu_idx[k - 1]:
self.xpt[k - 1, k] = -options[Options.RHOBEG]
else:
self.xpt[k - 1, k] = options[Options.RHOBEG]
elif k <= 2 * pb.n:
if very_close_xl_idx[k - pb.n - 1]:
self.xpt[k - pb.n - 1, k] = 2.0 * options[Options.RHOBEG]
elif very_close_xu_idx[k - pb.n - 1]:
self.xpt[k - pb.n - 1, k] = -2.0 * options[Options.RHOBEG]
else:
self.xpt[k - pb.n - 1, k] = -options[Options.RHOBEG]
else:
spread = (k - pb.n - 1) // pb.n
k1 = k - (1 + spread) * pb.n - 1
k2 = (k1 + spread) % pb.n
self.xpt[k1, k] = self.xpt[k1, k1 + 1]
self.xpt[k2, k] = self.xpt[k2, k2 + 1]
@property
def n(self):
"""
Number of variables.
Returns
-------
int
Number of variables.
"""
return self.xpt.shape[0]
@property
def npt(self):
"""
Number of interpolation points.
Returns
-------
int
Number of interpolation points.
"""
return self.xpt.shape[1]
@property
def xpt(self):
"""
Interpolation points.
Returns
-------
`numpy.ndarray`, shape (n, npt)
Interpolation points.
"""
return self._xpt
@xpt.setter
def xpt(self, xpt):
"""
Set the interpolation points.
Parameters
----------
xpt : `numpy.ndarray`, shape (n, npt)
New interpolation points.
"""
if self._debug:
assert xpt.shape == (
self.n,
self.npt,
), "The shape of `xpt` is not valid."
self._xpt = xpt
@property
def x_base(self):
"""
Base point around which the models are expanded.
Returns
-------
`numpy.ndarray`, shape (n,)
Base point around which the models are expanded.
"""
return self._x_base
@x_base.setter
def x_base(self, x_base):
"""
Set the base point around which the models are expanded.
Parameters
----------
x_base : `numpy.ndarray`, shape (n,)
New base point around which the models are expanded.
"""
if self._debug:
assert x_base.shape == (
self.n,
), "The shape of `x_base` is not valid."
self._x_base = x_base
def point(self, k):
"""
Get the `k`-th interpolation point.
The return point is relative to the origin.
Parameters
----------
k : int
Index of the interpolation point.
Returns
-------
`numpy.ndarray`, shape (n,)
`k`-th interpolation point.
"""
if self._debug:
assert 0 <= k < self.npt, "The index `k` is not valid."
return self.x_base + self.xpt[:, k]
_cache = {"xpt": None, "a": None, "right_scaling": None, "eigh": None}
def build_system(interpolation):
"""
Build the left-hand side matrix of the interpolation system. The
matrix below stores W * diag(right_scaling),
where W is the theoretical matrix of the interpolation system. The
right scaling matrices is chosen to keep the elements in
the matrix well-balanced.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
"""
# Compute the scaled directions from the base point to the
# interpolation points. We scale the directions to avoid numerical
# difficulties.
if _cache["xpt"] is not None and np.array_equal(
interpolation.xpt, _cache["xpt"]
):
return _cache["a"], _cache["right_scaling"], _cache["eigh"]
scale = np.max(np.linalg.norm(interpolation.xpt, axis=0), initial=EPS)
xpt_scale = interpolation.xpt / scale
n, npt = xpt_scale.shape
a = np.zeros((npt + n + 1, npt + n + 1))
a[:npt, :npt] = 0.5 * (xpt_scale.T @ xpt_scale) ** 2.0
a[:npt, npt] = 1.0
a[:npt, npt + 1:] = xpt_scale.T
a[npt, :npt] = 1.0
a[npt + 1:, :npt] = xpt_scale
# Build the left and right scaling diagonal matrices.
right_scaling = np.empty(npt + n + 1)
right_scaling[:npt] = 1.0 / scale**2.0
right_scaling[npt] = scale**2.0
right_scaling[npt + 1:] = scale
eig_values, eig_vectors = eigh(a, check_finite=False)
_cache["xpt"] = np.copy(interpolation.xpt)
_cache["a"] = np.copy(a)
_cache["right_scaling"] = np.copy(right_scaling)
_cache["eigh"] = (eig_values, eig_vectors)
return a, right_scaling, (eig_values, eig_vectors)
class Quadratic:
"""
Quadratic model.
This class stores the Hessian matrix of the quadratic model using the
implicit/explicit representation designed by Powell for NEWUOA [1]_.
References
----------
.. [1] M. J. D. Powell. The NEWUOA software for unconstrained optimization
without derivatives. In G. Di Pillo and M. Roma, editors, *Large-Scale
Nonlinear Optimization*, volume 83 of Nonconvex Optim. Appl., pages
255--297. Springer, Boston, MA, USA, 2006. `doi:10.1007/0-387-30065-1_16
<https://doi.org/10.1007/0-387-30065-1_16>`_.
"""
def __init__(self, interpolation, values, debug):
"""
Initialize the quadratic model.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
values : `numpy.ndarray`, shape (npt,)
Values of the interpolated function at the interpolation points.
debug : bool
Whether to make debugging tests during the execution.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
self._debug = debug
if self._debug:
assert values.shape == (
interpolation.npt,
), "The shape of `values` is not valid."
if interpolation.npt < interpolation.n + 1:
raise ValueError(
f"The number of interpolation points must be at least "
f"{interpolation.n + 1}."
)
self._const, self._grad, self._i_hess, _ = self._get_model(
interpolation,
values,
)
self._e_hess = np.zeros((self.n, self.n))
def __call__(self, x, interpolation):
"""
Evaluate the quadratic model at a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which the quadratic model is evaluated.
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
Returns
-------
float
Value of the quadratic model at `x`.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
x_diff = x - interpolation.x_base
return (
self._const
+ self._grad @ x_diff
+ 0.5
* (
self._i_hess @ (interpolation.xpt.T @ x_diff) ** 2.0
+ x_diff @ self._e_hess @ x_diff
)
)
@property
def n(self):
"""
Number of variables.
Returns
-------
int
Number of variables.
"""
return self._grad.size
@property
def npt(self):
"""
Number of interpolation points used to define the quadratic model.
Returns
-------
int
Number of interpolation points used to define the quadratic model.
"""
return self._i_hess.size
def grad(self, x, interpolation):
"""
Evaluate the gradient of the quadratic model at a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which the gradient of the quadratic model is evaluated.
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
Returns
-------
`numpy.ndarray`, shape (n,)
Gradient of the quadratic model at `x`.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
x_diff = x - interpolation.x_base
return self._grad + self.hess_prod(x_diff, interpolation)
def hess(self, interpolation):
"""
Evaluate the Hessian matrix of the quadratic model.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
Returns
-------
`numpy.ndarray`, shape (n, n)
Hessian matrix of the quadratic model.
"""
return self._e_hess + interpolation.xpt @ (
self._i_hess[:, np.newaxis] * interpolation.xpt.T
)
def hess_prod(self, v, interpolation):
"""
Evaluate the right product of the Hessian matrix of the quadratic model
with a given vector.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Vector with which the Hessian matrix of the quadratic model is
multiplied from the right.
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
Returns
-------
`numpy.ndarray`, shape (n,)
Right product of the Hessian matrix of the quadratic model with
`v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
return self._e_hess @ v + interpolation.xpt @ (
self._i_hess * (interpolation.xpt.T @ v)
)
def curv(self, v, interpolation):
"""
Evaluate the curvature of the quadratic model along a given direction.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Direction along which the curvature of the quadratic model is
evaluated.
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
Returns
-------
float
Curvature of the quadratic model along `v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
return (
v @ self._e_hess @ v
+ self._i_hess @ (interpolation.xpt.T @ v) ** 2.0
)
def update(self, interpolation, k_new, dir_old, values_diff):
"""
Update the quadratic model.
This method applies the derivative-free symmetric Broyden update to the
quadratic model. The `knew`-th interpolation point must be updated
before calling this method.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Updated interpolation set.
k_new : int
Index of the updated interpolation point.
dir_old : `numpy.ndarray`, shape (n,)
Value of ``interpolation.xpt[:, k_new]`` before the update.
values_diff : `numpy.ndarray`, shape (npt,)
Differences between the values of the interpolated nonlinear
function and the previous quadratic model at the updated
interpolation points.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
if self._debug:
assert 0 <= k_new < self.npt, "The index `k_new` is not valid."
assert dir_old.shape == (
self.n,
), "The shape of `dir_old` is not valid."
assert values_diff.shape == (
self.npt,
), "The shape of `values_diff` is not valid."
# Forward the k_new-th element of the implicit Hessian matrix to the
# explicit Hessian matrix. This must be done because the implicit
# Hessian matrix is related to the interpolation points, and the
# k_new-th interpolation point is modified.
self._e_hess += self._i_hess[k_new] * np.outer(dir_old, dir_old)
self._i_hess[k_new] = 0.0
# Update the quadratic model.
const, grad, i_hess, ill_conditioned = self._get_model(
interpolation,
values_diff,
)
self._const += const
self._grad += grad
self._i_hess += i_hess
return ill_conditioned
def shift_x_base(self, interpolation, new_x_base):
"""
Shift the point around which the quadratic model is defined.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Previous interpolation set.
new_x_base : `numpy.ndarray`, shape (n,)
Point that will replace ``interpolation.x_base``.
"""
if self._debug:
assert new_x_base.shape == (
self.n,
), "The shape of `new_x_base` is not valid."
self._const = self(new_x_base, interpolation)
self._grad = self.grad(new_x_base, interpolation)
shift = new_x_base - interpolation.x_base
update = np.outer(
shift,
(interpolation.xpt - 0.5 * shift[:, np.newaxis]) @ self._i_hess,
)
self._e_hess += update + update.T
@staticmethod
def solve_systems(interpolation, rhs):
"""
Solve the interpolation systems.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
rhs : `numpy.ndarray`, shape (npt + n + 1, m)
Right-hand side vectors of the ``m`` interpolation systems.
Returns
-------
`numpy.ndarray`, shape (npt + n + 1, m)
Solutions of the interpolation systems.
`numpy.ndarray`, shape (m, )
Whether the interpolation systems are ill-conditioned.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation systems are ill-defined.
"""
n, npt = interpolation.xpt.shape
assert (
rhs.ndim == 2 and rhs.shape[0] == npt + n + 1
), "The shape of `rhs` is not valid."
# Build the left-hand side matrix of the interpolation system. The
# matrix below stores diag(left_scaling) * W * diag(right_scaling),
# where W is the theoretical matrix of the interpolation system. The
# left and right scaling matrices are chosen to keep the elements in
# the matrix well-balanced.
a, right_scaling, eig = build_system(interpolation)
# Build the solution. After a discussion with Mike Saunders and Alexis
# Montoison during their visit to the Hong Kong Polytechnic University
# in 2024, we decided to use the eigendecomposition of the symmetric
# matrix a. This is more stable than the previously employed LBL
# decomposition, and allows us to directly detect ill-conditioning of
# the system and to build the least-squares solution if necessary.
# Numerical experiments have shown that this strategy improves the
# performance of the solver.
rhs_scaled = rhs * right_scaling[:, np.newaxis]
if not (np.all(np.isfinite(a)) and np.all(np.isfinite(rhs_scaled))):
raise np.linalg.LinAlgError(
"The interpolation system is ill-defined."
)
# calculated in build_system
eig_values, eig_vectors = eig
large_eig_values = np.abs(eig_values) > EPS
eig_vectors = eig_vectors[:, large_eig_values]
inv_eig_values = 1.0 / eig_values[large_eig_values]
ill_conditioned = ~np.all(large_eig_values, 0)
left_scaled_solutions = eig_vectors @ (
(eig_vectors.T @ rhs_scaled) * inv_eig_values[:, np.newaxis]
)
return (
left_scaled_solutions * right_scaling[:, np.newaxis],
ill_conditioned,
)
@staticmethod
def _get_model(interpolation, values):
"""
Solve the interpolation system.
Parameters
----------
interpolation : `cobyqa.models.Interpolation`
Interpolation set.
values : `numpy.ndarray`, shape (npt,)
Values of the interpolated function at the interpolation points.
Returns
-------
float
Constant term of the quadratic model.
`numpy.ndarray`, shape (n,)
Gradient of the quadratic model at ``interpolation.x_base``.
`numpy.ndarray`, shape (npt,)
Implicit Hessian matrix of the quadratic model.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
assert values.shape == (
interpolation.npt,
), "The shape of `values` is not valid."
n, npt = interpolation.xpt.shape
x, ill_conditioned = Quadratic.solve_systems(
interpolation,
np.block(
[
[
values,
np.zeros(n + 1),
]
]
).T,
)
return x[npt, 0], x[npt + 1:, 0], x[:npt, 0], ill_conditioned
class Models:
"""
Models for a nonlinear optimization problem.
"""
def __init__(self, pb, options):
"""
Initialize the models.
Parameters
----------
pb : `cobyqa.problem.Problem`
Problem to be solved.
options : dict
Options of the solver.
Raises
------
`cobyqa.utils.MaxEvalError`
If the maximum number of evaluations is reached.
`cobyqa.utils.TargetSuccess`
If a nearly feasible point has been found with an objective
function value below the target.
`cobyqa.utils.FeasibleSuccess`
If a feasible point has been found for a feasibility problem.
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
# Set the initial interpolation set.
self._debug = options[Options.DEBUG]
self._interpolation = Interpolation(pb, options)
# Evaluate the nonlinear functions at the initial interpolation points.
x_eval = self.interpolation.point(0)
fun_init, cub_init, ceq_init = pb(x_eval)
self._fun_val = np.full(options[Options.NPT], np.nan)
self._cub_val = np.full((options[Options.NPT], cub_init.size), np.nan)
self._ceq_val = np.full((options[Options.NPT], ceq_init.size), np.nan)
for k in range(options[Options.NPT]):
if k >= options[Options.MAX_EVAL]:
raise MaxEvalError
if k == 0:
self.fun_val[k] = fun_init
self.cub_val[k, :] = cub_init
self.ceq_val[k, :] = ceq_init
else:
x_eval = self.interpolation.point(k)
self.fun_val[k], self.cub_val[k, :], self.ceq_val[k, :] = pb(
x_eval
)
# Stop the iterations if the problem is a feasibility problem and
# the current interpolation point is feasible.
if (
pb.is_feasibility
and pb.maxcv(
self.interpolation.point(k),
self.cub_val[k, :],
self.ceq_val[k, :],
)
<= options[Options.FEASIBILITY_TOL]
):
raise FeasibleSuccess
# Stop the iterations if the current interpolation point is nearly
# feasible and has an objective function value below the target.
if (
self._fun_val[k] <= options[Options.TARGET]
and pb.maxcv(
self.interpolation.point(k),
self.cub_val[k, :],
self.ceq_val[k, :],
)
<= options[Options.FEASIBILITY_TOL]
):
raise TargetSuccess
# Build the initial quadratic models.
self._fun = Quadratic(
self.interpolation,
self._fun_val,
options[Options.DEBUG],
)
self._cub = np.empty(self.m_nonlinear_ub, dtype=Quadratic)
self._ceq = np.empty(self.m_nonlinear_eq, dtype=Quadratic)
for i in range(self.m_nonlinear_ub):
self._cub[i] = Quadratic(
self.interpolation,
self.cub_val[:, i],
options[Options.DEBUG],
)
for i in range(self.m_nonlinear_eq):
self._ceq[i] = Quadratic(
self.interpolation,
self.ceq_val[:, i],
options[Options.DEBUG],
)
if self._debug:
self._check_interpolation_conditions()
@property
def n(self):
"""
Dimension of the problem.
Returns
-------
int
Dimension of the problem.
"""
return self.interpolation.n
@property
def npt(self):
"""
Number of interpolation points.
Returns
-------
int
Number of interpolation points.
"""
return self.interpolation.npt
@property
def m_nonlinear_ub(self):
"""
Number of nonlinear inequality constraints.
Returns
-------
int
Number of nonlinear inequality constraints.
"""
return self.cub_val.shape[1]
@property
def m_nonlinear_eq(self):
"""
Number of nonlinear equality constraints.
Returns
-------
int
Number of nonlinear equality constraints.
"""
return self.ceq_val.shape[1]
@property
def interpolation(self):
"""
Interpolation set.
Returns
-------
`cobyqa.models.Interpolation`
Interpolation set.
"""
return self._interpolation
@property
def fun_val(self):
"""
Values of the objective function at the interpolation points.
Returns
-------
`numpy.ndarray`, shape (npt,)
Values of the objective function at the interpolation points.
"""
return self._fun_val
@property
def cub_val(self):
"""
Values of the nonlinear inequality constraint functions at the
interpolation points.
Returns
-------
`numpy.ndarray`, shape (npt, m_nonlinear_ub)
Values of the nonlinear inequality constraint functions at the
interpolation points.
"""
return self._cub_val
@property
def ceq_val(self):
"""
Values of the nonlinear equality constraint functions at the
interpolation points.
Returns
-------
`numpy.ndarray`, shape (npt, m_nonlinear_eq)
Values of the nonlinear equality constraint functions at the
interpolation points.
"""
return self._ceq_val
def fun(self, x):
"""
Evaluate the quadratic model of the objective function at a given
point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the quadratic model of the objective
function.
Returns
-------
float
Value of the quadratic model of the objective function at `x`.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
return self._fun(x, self.interpolation)
def fun_grad(self, x):
"""
Evaluate the gradient of the quadratic model of the objective function
at a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the gradient of the quadratic model of
the objective function.
Returns
-------
`numpy.ndarray`, shape (n,)
Gradient of the quadratic model of the objective function at `x`.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
return self._fun.grad(x, self.interpolation)
def fun_hess(self):
"""
Evaluate the Hessian matrix of the quadratic model of the objective
function.
Returns
-------
`numpy.ndarray`, shape (n, n)
Hessian matrix of the quadratic model of the objective function.
"""
return self._fun.hess(self.interpolation)
def fun_hess_prod(self, v):
"""
Evaluate the right product of the Hessian matrix of the quadratic model
of the objective function with a given vector.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Vector with which the Hessian matrix of the quadratic model of the
objective function is multiplied from the right.
Returns
-------
`numpy.ndarray`, shape (n,)
Right product of the Hessian matrix of the quadratic model of the
objective function with `v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
return self._fun.hess_prod(v, self.interpolation)
def fun_curv(self, v):
"""
Evaluate the curvature of the quadratic model of the objective function
along a given direction.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Direction along which the curvature of the quadratic model of the
objective function is evaluated.
Returns
-------
float
Curvature of the quadratic model of the objective function along
`v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
return self._fun.curv(v, self.interpolation)
def fun_alt_grad(self, x):
"""
Evaluate the gradient of the alternative quadratic model of the
objective function at a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the gradient of the alternative
quadratic model of the objective function.
Returns
-------
`numpy.ndarray`, shape (n,)
Gradient of the alternative quadratic model of the objective
function at `x`.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
model = Quadratic(self.interpolation, self.fun_val, self._debug)
return model.grad(x, self.interpolation)
def cub(self, x, mask=None):
"""
Evaluate the quadratic models of the nonlinear inequality functions at
a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the quadratic models of the nonlinear
inequality functions.
mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Values of the quadratic model of the nonlinear inequality
functions.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_ub,
), "The shape of `mask` is not valid."
return np.array(
[model(x, self.interpolation) for model in self._get_cub(mask)]
)
def cub_grad(self, x, mask=None):
"""
Evaluate the gradients of the quadratic models of the nonlinear
inequality functions at a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the gradients of the quadratic models of
the nonlinear inequality functions.
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Gradients of the quadratic model of the nonlinear inequality
functions.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_ub,
), "The shape of `mask` is not valid."
return np.reshape(
[model.grad(x, self.interpolation)
for model in self._get_cub(mask)],
(-1, self.n),
)
def cub_hess(self, mask=None):
"""
Evaluate the Hessian matrices of the quadratic models of the nonlinear
inequality functions.
Parameters
----------
mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Hessian matrices of the quadratic models of the nonlinear
inequality functions.
"""
if self._debug:
assert mask is None or mask.shape == (
self.m_nonlinear_ub,
), "The shape of `mask` is not valid."
return np.reshape(
[model.hess(self.interpolation) for model in self._get_cub(mask)],
(-1, self.n, self.n),
)
def cub_hess_prod(self, v, mask=None):
"""
Evaluate the right product of the Hessian matrices of the quadratic
models of the nonlinear inequality functions with a given vector.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Vector with which the Hessian matrices of the quadratic models of
the nonlinear inequality functions are multiplied from the right.
mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Right products of the Hessian matrices of the quadratic models of
the nonlinear inequality functions with `v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_ub,
), "The shape of `mask` is not valid."
return np.reshape(
[
model.hess_prod(v, self.interpolation)
for model in self._get_cub(mask)
],
(-1, self.n),
)
def cub_curv(self, v, mask=None):
"""
Evaluate the curvature of the quadratic models of the nonlinear
inequality functions along a given direction.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Direction along which the curvature of the quadratic models of the
nonlinear inequality functions is evaluated.
mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Curvature of the quadratic models of the nonlinear inequality
functions along `v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_ub,
), "The shape of `mask` is not valid."
return np.array(
[model.curv(v, self.interpolation)
for model in self._get_cub(mask)]
)
def ceq(self, x, mask=None):
"""
Evaluate the quadratic models of the nonlinear equality functions at a
given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the quadratic models of the nonlinear
equality functions.
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Values of the quadratic model of the nonlinear equality functions.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_eq,
), "The shape of `mask` is not valid."
return np.array(
[model(x, self.interpolation) for model in self._get_ceq(mask)]
)
def ceq_grad(self, x, mask=None):
"""
Evaluate the gradients of the quadratic models of the nonlinear
equality functions at a given point.
Parameters
----------
x : `numpy.ndarray`, shape (n,)
Point at which to evaluate the gradients of the quadratic models of
the nonlinear equality functions.
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Gradients of the quadratic model of the nonlinear equality
functions.
"""
if self._debug:
assert x.shape == (self.n,), "The shape of `x` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_eq,
), "The shape of `mask` is not valid."
return np.reshape(
[model.grad(x, self.interpolation)
for model in self._get_ceq(mask)],
(-1, self.n),
)
def ceq_hess(self, mask=None):
"""
Evaluate the Hessian matrices of the quadratic models of the nonlinear
equality functions.
Parameters
----------
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Hessian matrices of the quadratic models of the nonlinear equality
functions.
"""
if self._debug:
assert mask is None or mask.shape == (
self.m_nonlinear_eq,
), "The shape of `mask` is not valid."
return np.reshape(
[model.hess(self.interpolation) for model in self._get_ceq(mask)],
(-1, self.n, self.n),
)
def ceq_hess_prod(self, v, mask=None):
"""
Evaluate the right product of the Hessian matrices of the quadratic
models of the nonlinear equality functions with a given vector.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Vector with which the Hessian matrices of the quadratic models of
the nonlinear equality functions are multiplied from the right.
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Right products of the Hessian matrices of the quadratic models of
the nonlinear equality functions with `v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_eq,
), "The shape of `mask` is not valid."
return np.reshape(
[
model.hess_prod(v, self.interpolation)
for model in self._get_ceq(mask)
],
(-1, self.n),
)
def ceq_curv(self, v, mask=None):
"""
Evaluate the curvature of the quadratic models of the nonlinear
equality functions along a given direction.
Parameters
----------
v : `numpy.ndarray`, shape (n,)
Direction along which the curvature of the quadratic models of the
nonlinear equality functions is evaluated.
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to consider.
Returns
-------
`numpy.ndarray`
Curvature of the quadratic models of the nonlinear equality
functions along `v`.
"""
if self._debug:
assert v.shape == (self.n,), "The shape of `v` is not valid."
assert mask is None or mask.shape == (
self.m_nonlinear_eq,
), "The shape of `mask` is not valid."
return np.array(
[model.curv(v, self.interpolation)
for model in self._get_ceq(mask)]
)
def reset_models(self):
"""
Set the quadratic models of the objective function, nonlinear
inequality constraints, and nonlinear equality constraints to the
alternative quadratic models.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
self._fun = Quadratic(self.interpolation, self.fun_val, self._debug)
for i in range(self.m_nonlinear_ub):
self._cub[i] = Quadratic(
self.interpolation,
self.cub_val[:, i],
self._debug,
)
for i in range(self.m_nonlinear_eq):
self._ceq[i] = Quadratic(
self.interpolation,
self.ceq_val[:, i],
self._debug,
)
if self._debug:
self._check_interpolation_conditions()
def update_interpolation(self, k_new, x_new, fun_val, cub_val, ceq_val):
"""
Update the interpolation set.
This method updates the interpolation set by replacing the `knew`-th
interpolation point with `xnew`. It also updates the function values
and the quadratic models.
Parameters
----------
k_new : int
Index of the updated interpolation point.
x_new : `numpy.ndarray`, shape (n,)
New interpolation point. Its value is interpreted as relative to
the origin, not the base point.
fun_val : float
Value of the objective function at `x_new`.
Objective function value at `x_new`.
cub_val : `numpy.ndarray`, shape (m_nonlinear_ub,)
Values of the nonlinear inequality constraints at `x_new`.
ceq_val : `numpy.ndarray`, shape (m_nonlinear_eq,)
Values of the nonlinear equality constraints at `x_new`.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
"""
if self._debug:
assert 0 <= k_new < self.npt, "The index `k_new` is not valid."
assert x_new.shape == (self.n,), \
"The shape of `x_new` is not valid."
assert isinstance(fun_val, float), \
"The function value is not valid."
assert cub_val.shape == (
self.m_nonlinear_ub,
), "The shape of `cub_val` is not valid."
assert ceq_val.shape == (
self.m_nonlinear_eq,
), "The shape of `ceq_val` is not valid."
# Compute the updates in the interpolation conditions.
fun_diff = np.zeros(self.npt)
cub_diff = np.zeros(self.cub_val.shape)
ceq_diff = np.zeros(self.ceq_val.shape)
fun_diff[k_new] = fun_val - self.fun(x_new)
cub_diff[k_new, :] = cub_val - self.cub(x_new)
ceq_diff[k_new, :] = ceq_val - self.ceq(x_new)
# Update the function values.
self.fun_val[k_new] = fun_val
self.cub_val[k_new, :] = cub_val
self.ceq_val[k_new, :] = ceq_val
# Update the interpolation set.
dir_old = np.copy(self.interpolation.xpt[:, k_new])
self.interpolation.xpt[:, k_new] = x_new - self.interpolation.x_base
# Update the quadratic models.
ill_conditioned = self._fun.update(
self.interpolation,
k_new,
dir_old,
fun_diff,
)
for i in range(self.m_nonlinear_ub):
ill_conditioned = ill_conditioned or self._cub[i].update(
self.interpolation,
k_new,
dir_old,
cub_diff[:, i],
)
for i in range(self.m_nonlinear_eq):
ill_conditioned = ill_conditioned or self._ceq[i].update(
self.interpolation,
k_new,
dir_old,
ceq_diff[:, i],
)
if self._debug:
self._check_interpolation_conditions()
return ill_conditioned
def determinants(self, x_new, k_new=None):
"""
Compute the normalized determinants of the new interpolation systems.
Parameters
----------
x_new : `numpy.ndarray`, shape (n,)
New interpolation point. Its value is interpreted as relative to
the origin, not the base point.
k_new : int, optional
Index of the updated interpolation point. If `k_new` is not
specified, all the possible determinants are computed.
Returns
-------
{float, `numpy.ndarray`, shape (npt,)}
Determinant(s) of the new interpolation system.
Raises
------
`numpy.linalg.LinAlgError`
If the interpolation system is ill-defined.
Notes
-----
The determinants are normalized by the determinant of the current
interpolation system. For stability reasons, the calculations are done
using the formula (2.12) in [1]_.
References
----------
.. [1] M. J. D. Powell. On updating the inverse of a KKT matrix.
Technical Report DAMTP 2004/NA01, Department of Applied Mathematics
and Theoretical Physics, University of Cambridge, Cambridge, UK,
2004.
"""
if self._debug:
assert x_new.shape == (self.n,), \
"The shape of `x_new` is not valid."
assert (
k_new is None or 0 <= k_new < self.npt
), "The index `k_new` is not valid."
# Compute the values independent of k_new.
shift = x_new - self.interpolation.x_base
new_col = np.empty((self.npt + self.n + 1, 1))
new_col[: self.npt, 0] = (
0.5 * (self.interpolation.xpt.T @ shift) ** 2.0)
new_col[self.npt, 0] = 1.0
new_col[self.npt + 1:, 0] = shift
inv_new_col = Quadratic.solve_systems(self.interpolation, new_col)[0]
beta = 0.5 * (shift @ shift) ** 2.0 - new_col[:, 0] @ inv_new_col[:, 0]
# Compute the values that depend on k.
if k_new is None:
coord_vec = np.eye(self.npt + self.n + 1, self.npt)
alpha = np.diag(
Quadratic.solve_systems(
self.interpolation,
coord_vec,
)[0]
)
tau = inv_new_col[: self.npt, 0]
else:
coord_vec = np.eye(self.npt + self.n + 1, 1, -k_new)
alpha = Quadratic.solve_systems(
self.interpolation,
coord_vec,
)[
0
][k_new, 0]
tau = inv_new_col[k_new, 0]
return alpha * beta + tau**2.0
def shift_x_base(self, new_x_base, options):
"""
Shift the base point without changing the interpolation set.
Parameters
----------
new_x_base : `numpy.ndarray`, shape (n,)
New base point.
options : dict
Options of the solver.
"""
if self._debug:
assert new_x_base.shape == (
self.n,
), "The shape of `new_x_base` is not valid."
# Update the models.
self._fun.shift_x_base(self.interpolation, new_x_base)
for model in self._cub:
model.shift_x_base(self.interpolation, new_x_base)
for model in self._ceq:
model.shift_x_base(self.interpolation, new_x_base)
# Update the base point and the interpolation points.
shift = new_x_base - self.interpolation.x_base
self.interpolation.x_base += shift
self.interpolation.xpt -= shift[:, np.newaxis]
if options[Options.DEBUG]:
self._check_interpolation_conditions()
def _get_cub(self, mask=None):
"""
Get the quadratic models of the nonlinear inequality constraints.
Parameters
----------
mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
Mask of the quadratic models to return.
Returns
-------
`numpy.ndarray`
Quadratic models of the nonlinear inequality constraints.
"""
return self._cub if mask is None else self._cub[mask]
def _get_ceq(self, mask=None):
"""
Get the quadratic models of the nonlinear equality constraints.
Parameters
----------
mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
Mask of the quadratic models to return.
Returns
-------
`numpy.ndarray`
Quadratic models of the nonlinear equality constraints.
"""
return self._ceq if mask is None else self._ceq[mask]
def _check_interpolation_conditions(self):
"""
Check the interpolation conditions of all quadratic models.
"""
error_fun = 0.0
error_cub = 0.0
error_ceq = 0.0
for k in range(self.npt):
error_fun = np.max(
[
error_fun,
np.abs(
self.fun(self.interpolation.point(k)) - self.fun_val[k]
),
]
)
error_cub = np.max(
np.abs(
self.cub(self.interpolation.point(k)) - self.cub_val[k, :]
),
initial=error_cub,
)
error_ceq = np.max(
np.abs(
self.ceq(self.interpolation.point(k)) - self.ceq_val[k, :]
),
initial=error_ceq,
)
tol = 10.0 * np.sqrt(EPS) * max(self.n, self.npt)
if error_fun > tol * np.max(np.abs(self.fun_val), initial=1.0):
warnings.warn(
"The interpolation conditions for the objective function are "
"not satisfied.",
RuntimeWarning,
2,
)
if error_cub > tol * np.max(np.abs(self.cub_val), initial=1.0):
warnings.warn(
"The interpolation conditions for the inequality constraint "
"function are not satisfied.",
RuntimeWarning,
2,
)
if error_ceq > tol * np.max(np.abs(self.ceq_val), initial=1.0):
warnings.warn(
"The interpolation conditions for the equality constraint "
"function are not satisfied.",
RuntimeWarning,
2,
)