608 lines
20 KiB
Python
608 lines
20 KiB
Python
# Copyright 2024 DeepMind Technologies Limited
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ==============================================================================
|
|
"""Nonlinear Least Squares minimization with box bounds."""
|
|
|
|
import abc
|
|
import dataclasses
|
|
import enum
|
|
import time
|
|
from typing import Callable, List, Optional, Sequence, TextIO, Tuple, Union
|
|
|
|
import mujoco
|
|
import numpy as np
|
|
|
|
|
|
class Verbosity(enum.Enum):
|
|
SILENT = 0
|
|
FINAL = 1
|
|
ITER = 2
|
|
FULLITER = 3
|
|
|
|
|
|
class Status(enum.Enum):
|
|
FACTORIZATION_FAILED = enum.auto()
|
|
NO_IMPROVEMENT = enum.auto()
|
|
MAX_ITER = enum.auto()
|
|
DX_TOL = enum.auto()
|
|
G_TOL = enum.auto()
|
|
|
|
|
|
_STATUS_MESSAGE = {
|
|
Status.FACTORIZATION_FAILED: 'factorization failed.',
|
|
Status.NO_IMPROVEMENT: 'insufficient reduction.',
|
|
Status.MAX_ITER: 'maximum iterations reached.',
|
|
Status.DX_TOL: 'norm(dx) < tol.',
|
|
Status.G_TOL: 'norm(gradient) < tol.',
|
|
}
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class IterLog:
|
|
"""Log of a single iteration of the non-linear least-squares solver.
|
|
|
|
Attributes:
|
|
candidate: Value of the decision variable at the beginning this iteration.
|
|
objective: Value of the objective at the candidate.
|
|
reduction: Reduction of the objective during this iteration.
|
|
regularizer: Value of the regularizer used for this iteration.
|
|
residual: Optional value of the residual at the candidate.
|
|
jacobian: Optional value of the Jacobian at the candidate.
|
|
grad: Optional value of the gradient at the candidate.
|
|
step: Optional change in decision variable during this iteration.
|
|
"""
|
|
|
|
candidate: np.ndarray
|
|
objective: np.float64
|
|
reduction: np.float64
|
|
regularizer: np.float64
|
|
residual: Optional[np.ndarray] = None
|
|
jacobian: Optional[np.ndarray] = None
|
|
grad: Optional[np.ndarray] = None
|
|
step: Optional[np.ndarray] = None
|
|
|
|
|
|
class Norm(abc.ABC):
|
|
"""Abstract interface for norm functions, measuring the magnitude of vectors.
|
|
|
|
Key Concepts:
|
|
|
|
* Norm Value: The value of the norm for a given input vector.
|
|
* Gradient and Hessian: The gradient (first derivative) and Hessian (second
|
|
derivative) of the norm function with respect to the input vector.
|
|
|
|
Subclasses Must Implement:
|
|
|
|
* `value(self, r: np.ndarray)`: Computes and returns the norm value for the
|
|
input vector `r`.
|
|
* `grad_hess(self, r: np.ndarray, proj: np.ndarray)`: Computes and returns
|
|
both the gradient and Hessian of the norm at `r`, projected onto `proj`.
|
|
The reason we ask the user to perform the projection themselves is that
|
|
norm Hessians are often large and sparse, and the "sandwich" projection
|
|
operator `proj.T @ hess @ proj` can be computed efficiently by taking the
|
|
specific norm structure into account.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def value(self, r: np.ndarray) -> np.float64:
|
|
"""Returns the value of the norm at the input vector `y = norm(r)`."""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def grad_hess(self, r: np.ndarray, proj: np.ndarray):
|
|
"""Computes the projected gradient and Hessian of the norm at `r`.
|
|
|
|
Args:
|
|
r: A NumPy column vector (nr x 1).
|
|
proj: A pre-computed projection matrix (nr x nx).
|
|
|
|
Returns:
|
|
A tuple containing:
|
|
* Projected gradient: proj.T @ (d_norm/d_r).
|
|
* Projected Hessian: proj.T @ (d^2_norm/d_r^2) @ proj.
|
|
"""
|
|
pass
|
|
|
|
|
|
class Quadratic(Norm):
|
|
"""Implementation of the quadratic norm."""
|
|
|
|
def value(self, r: np.ndarray):
|
|
"""Returns the quadratic norm of `r`."""
|
|
return 0.5 * (r.T @ r).item()
|
|
|
|
def grad_hess(self, r: np.ndarray, proj: np.ndarray):
|
|
"""Computes the projected gradient and Hessian of the quadratic norm at `r`.
|
|
|
|
Args:
|
|
r: A NumPy column vector (nr x 1).
|
|
proj: A pre-computed projection matrix (nr x nx).
|
|
|
|
Returns:
|
|
A tuple containing:
|
|
* Projected gradient: `proj.T @ r`.
|
|
* Projected Hessian: `proj.T @ proj`.
|
|
"""
|
|
grad = proj.T @ r
|
|
hess = proj.T @ proj # Notionally proj.T @ np.eye(r.size) @ proj
|
|
return grad, hess
|
|
|
|
|
|
def least_squares(
|
|
x0: np.ndarray,
|
|
residual: Callable[[np.ndarray], np.ndarray],
|
|
bounds: Optional[Sequence[np.ndarray]] = None,
|
|
jacobian: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None,
|
|
norm: Norm = Quadratic(),
|
|
eps: float = np.finfo(np.float64).eps ** 0.5,
|
|
mu_min: float = 1e-6,
|
|
mu_max: float = 1e8,
|
|
mu_factor: float = 10.0 ** 0.1,
|
|
xtol: float = 1e-8,
|
|
gtol: float = 1e-8,
|
|
max_iter: int = 100,
|
|
verbose: Union[Verbosity, int] = Verbosity.ITER,
|
|
output: Optional[TextIO] = None,
|
|
iter_callback: Optional[Callable[[List[IterLog]], None]] = None,
|
|
check_derivatives: bool = False,
|
|
x_scale: Optional[Union[float, np.ndarray, str]] = None,
|
|
) -> Tuple[np.ndarray, List[IterLog]]:
|
|
"""Nonlinear Least Squares minimization with box bounds.
|
|
|
|
Args:
|
|
x0: Initial guess
|
|
residual: Vectorized function returning the residual for 1 or more points.
|
|
bounds: Optional pair of lower and upper bounds on the solution.
|
|
jacobian: Optional function that returns Jacobian of the residual at a given
|
|
point and residual. If not given, `residual` will be finite-differenced.
|
|
norm: Norm object returning norm scalar or its projected gradient and
|
|
Hessian. See Norm class for detailed documentation.
|
|
eps: Perurbation used for automatic finite-differencing.
|
|
mu_min: Minimum value of the regularizer.
|
|
mu_max: Maximum value of the regularizer.
|
|
mu_factor: Factor for increasing or decreasing the regularizer.
|
|
xtol: Termination tolerance on relative step size.
|
|
gtol: Termination tolerance on gradient norm.
|
|
max_iter: Maximum number of iterations.
|
|
verbose: Verbosity level.
|
|
output: Optional file or StringIO to which to print messages.
|
|
iter_callback: Optional iteration callback, takes trace argument.
|
|
check_derivatives: Compare user-defined Jacobian and norm against fin-diff.
|
|
x_scale: Per-parameter scaling (matches scipy's ``x_scale``). Setting
|
|
``x_scale=D`` solves the problem in the change of variables ``z = x / D``
|
|
and un-scales the result. ``None`` (default) or ``1.0`` is no scaling.
|
|
``'jac'`` sets ``D_i = 1 / ||J(:, i)||`` at each iteration. An array of
|
|
shape ``(n,)`` (or a positive scalar) is used as ``D`` directly. Note
|
|
that ``mu``, ``mu_min`` and ``mu_max`` then act on the scaled subproblem.
|
|
|
|
Returns:
|
|
x: best solution found
|
|
trace: sequence of solution iterates.
|
|
"""
|
|
t_start_total = time.time()
|
|
|
|
# Convert verbosity to int.
|
|
verbose = Verbosity(verbose).value
|
|
|
|
# Constant for Armijo's sufficient-reduction rule
|
|
armijo_c1 = 1e-2
|
|
|
|
# Initialize locals.
|
|
status = Status.MAX_ITER
|
|
i = 0
|
|
n = x0.size
|
|
x = x0.astype(np.float64).reshape((n, 1))
|
|
xnew = np.zeros((n, 1))
|
|
dx = np.zeros((n, 1))
|
|
scratch = np.zeros((n, n + 7))
|
|
eps = np.float64(eps)
|
|
mu = np.float64(0.0) # Optimistically start with no regularization.
|
|
n_reduc = 0 # Number of sequential mu reductions.
|
|
|
|
# Resolve x_scale -> D of shape (n, 1). For 'jac', D is refreshed each iter;
|
|
# None means no scaling (D = 1).
|
|
adaptive_scale = isinstance(x_scale, str)
|
|
if adaptive_scale and x_scale != 'jac':
|
|
raise ValueError(
|
|
f"x_scale must be None, 'jac', a positive scalar, or array, got "
|
|
f'{x_scale!r}.'
|
|
)
|
|
if x_scale is None or adaptive_scale:
|
|
D = np.ones((n, 1))
|
|
else:
|
|
D = np.asarray(x_scale, dtype=np.float64)
|
|
if D.shape not in ((), (n,)):
|
|
raise ValueError(f'x_scale array must have shape ({n},), got {D.shape}.')
|
|
if not np.all(np.isfinite(D)) or np.any(D <= 0):
|
|
raise ValueError('x_scale must be positive and finite.')
|
|
D = (np.ones(n) * D).reshape(n, 1)
|
|
|
|
# Initialize logging.
|
|
trace = []
|
|
n_res = 0
|
|
n_jac = 0
|
|
t_res = 0.0
|
|
t_jac = 0.0
|
|
|
|
if mu_factor <= 1:
|
|
raise ValueError('mu_factor must be > 1.')
|
|
|
|
# Decrease mu agressively: sequential decreases grow exponentially.
|
|
def decrease_mu(mu, n_reduc):
|
|
dmu = (1 / mu_factor) ** (2**n_reduc)
|
|
mu = 0.0 if mu * dmu < mu_min else mu * dmu
|
|
n_reduc += 1
|
|
return mu, n_reduc
|
|
|
|
# Increase mu carefully: always increase by mu_factor.
|
|
def increase_mu(mu):
|
|
mu = max(mu_min, mu_factor * mu)
|
|
n_reduc = 0 # Reset n_reduc.
|
|
return mu, n_reduc
|
|
|
|
# Make local copy of bounds to avoid reshaping user input.
|
|
bounds = None if bounds is None else list(bounds)
|
|
if bounds is not None:
|
|
# Checks bounds.
|
|
if len(bounds) != 2:
|
|
raise ValueError('bounds must have 2 elements.')
|
|
if bounds[0].size != n or bounds[1].size != n:
|
|
raise ValueError('bounds must have the same size as x0.')
|
|
if not np.all(np.isfinite(bounds[0])) or not np.all(np.isfinite(bounds[1])):
|
|
raise ValueError('bounds must be finite.')
|
|
if not np.all(bounds[0] < bounds[1]):
|
|
raise ValueError('bounds[0] must be smaller than bounds[1].')
|
|
|
|
# Reshape and clip.
|
|
bounds[0] = bounds[0].reshape(n, 1)
|
|
bounds[1] = bounds[1].reshape(n, 1)
|
|
np.clip(x, bounds[0], bounds[1], out=x)
|
|
|
|
# Check for NaNs.
|
|
if not np.all(np.isfinite(x)):
|
|
raise ValueError('x0 must be finite.')
|
|
|
|
# Get initial residual.
|
|
t_start = time.time()
|
|
r = residual(x)
|
|
rnew = r
|
|
t_res += time.time() - t_start
|
|
n_res += 1
|
|
|
|
if r.dtype != np.float64:
|
|
raise ValueError('residual function must return float64 arrays.')
|
|
|
|
# Minimize.
|
|
for i in range(max_iter):
|
|
if status != Status.MAX_ITER:
|
|
break
|
|
|
|
# Get objective y.
|
|
y = norm.value(r)
|
|
|
|
# Get Jacobian jac.
|
|
t_start = time.time()
|
|
if jacobian is None:
|
|
jac, n_res = jacobian_fd(residual, x, r, eps, n_res, bounds)
|
|
t_res += time.time() - t_start
|
|
else:
|
|
jac = jacobian(x, r)
|
|
t_jac += time.time() - t_start
|
|
n_jac += 1
|
|
|
|
# Check user-provided Jacobian
|
|
if i == 0 and check_derivatives:
|
|
n_res = check_jacobian(residual, x, r, jac, eps, n_res, bounds, output)
|
|
|
|
# Check user-provided norm
|
|
if i == 0 and check_derivatives and not isinstance(norm, Quadratic):
|
|
check_norm(r, norm, eps, output)
|
|
|
|
# Refresh D for adaptive ('jac') scaling: column-norm preconditioner.
|
|
if adaptive_scale:
|
|
col_norms = np.linalg.norm(jac, axis=0)
|
|
D = (1.0 / np.maximum(col_norms, eps)).reshape(n, 1)
|
|
|
|
# Gradient/Hessian in scaled coords (jac * D.T scales columns).
|
|
grad, hess = norm.grad_hess(r, jac * D.T)
|
|
|
|
# Get free (unclamped) gradient.
|
|
if bounds is None:
|
|
grad_free = grad
|
|
else:
|
|
clamped_lower = (x == bounds[0]) & (grad > 0)
|
|
clamped_upper = (x == bounds[1]) & (grad < 0)
|
|
clamped = clamped_lower | clamped_upper
|
|
grad_free = grad[~clamped]
|
|
|
|
# Check termination condition on gradient norm.
|
|
g_norm = np.linalg.norm(grad_free)
|
|
if g_norm <= gtol:
|
|
status = Status.G_TOL
|
|
if g_norm == 0:
|
|
print('Zero gradient norm: exact minimum found?', file=output)
|
|
break
|
|
|
|
# Bounds relative to x, expressed in scaled coords (dz = dx / D).
|
|
dlower = None if bounds is None else (bounds[0] - x) / D
|
|
dupper = None if bounds is None else (bounds[1] - x) / D
|
|
|
|
# Find reduction satisfying Armijo's rule.
|
|
armijo = -1
|
|
reduction = 0.0
|
|
while armijo < 0:
|
|
# Increase mu until factorizable.
|
|
factorizable = False
|
|
while not factorizable:
|
|
n_free = mujoco.mju_boxQP(
|
|
dx, scratch, None, hess + mu * np.eye(n), grad, dlower, dupper
|
|
)
|
|
if n_free >= 0:
|
|
factorizable = True
|
|
elif mu >= mu_max:
|
|
status = Status.FACTORIZATION_FAILED
|
|
break
|
|
else:
|
|
mu, n_reduc = increase_mu(mu)
|
|
|
|
if status != Status.MAX_ITER:
|
|
break
|
|
|
|
# New candidate (D * dx is the x-space step).
|
|
xnew = x + D * dx
|
|
t_start = time.time()
|
|
rnew = residual(xnew)
|
|
t_res += time.time() - t_start
|
|
n_res += 1
|
|
|
|
# New objective, evaluate reduction.
|
|
ynew = norm.value(rnew)
|
|
reduction = y - ynew
|
|
armijo = reduction + armijo_c1 * (grad.T @ dx).item()
|
|
|
|
if armijo < 0:
|
|
if mu >= mu_max:
|
|
status = Status.NO_IMPROVEMENT
|
|
break
|
|
mu, n_reduc = increase_mu(mu)
|
|
|
|
if status != Status.MAX_ITER:
|
|
break
|
|
|
|
# Compute reduction ratio.
|
|
expected_reduction = -(grad.T @ dx + 0.5 * dx.T @ hess @ dx).item()
|
|
reduction_ratio = 0.0
|
|
if expected_reduction <= 0:
|
|
if verbose > Verbosity.SILENT.value:
|
|
if expected_reduction == 0:
|
|
print('Zero expected reduction: exact minimum found?', file=output)
|
|
elif expected_reduction < 0:
|
|
print('Negative expected reduction: should not occur.', file=output)
|
|
else:
|
|
reduction_ratio = reduction / expected_reduction
|
|
|
|
# Iteration message. Step printed in x-space (D * dx).
|
|
step = D * dx
|
|
dx_norm = np.linalg.norm(step)
|
|
if verbose >= Verbosity.ITER.value:
|
|
logmu = np.log10(mu) if mu > 0 else -np.inf
|
|
message = (
|
|
f'iter: {i:<3d} y: {y:<9.4g} log10mu: {logmu:>4.1f} '
|
|
f'ratio: {reduction_ratio:<7.2g} '
|
|
f'dx: {dx_norm:<7.2g} reduction: {reduction:<7.2g}'
|
|
)
|
|
print(message, file=output)
|
|
|
|
# Append log to trace, call iter_callback.
|
|
log = IterLog(candidate=x, objective=y, reduction=reduction, regularizer=mu)
|
|
if verbose >= Verbosity.FULLITER.value:
|
|
log = dataclasses.replace(
|
|
log, residual=r, jacobian=jac, grad=grad / D, step=step
|
|
)
|
|
trace.append(log)
|
|
if iter_callback is not None:
|
|
iter_callback(trace)
|
|
|
|
# Check termination condition on step norm.
|
|
if dx_norm < xtol * (xtol + np.linalg.norm(x)):
|
|
status = Status.DX_TOL
|
|
break
|
|
|
|
# Modify regularizer like in (Bazaraa, Sherali, and Shetty)
|
|
if reduction_ratio > 0.75:
|
|
mu, n_reduc = decrease_mu(mu, n_reduc)
|
|
elif reduction_ratio < 0.25:
|
|
mu, n_reduc = increase_mu(mu)
|
|
|
|
# Accept proposal.
|
|
x = xnew
|
|
r = rnew
|
|
|
|
# Append final log to trace, call iter_callback.
|
|
# Note: unlike other iter logs, values are computed at the end point.
|
|
yfinal = norm.value(r)
|
|
red = np.float64(0.0) # No reduction since we didn't take a step.
|
|
log = IterLog(candidate=x, objective=yfinal, reduction=red, regularizer=mu)
|
|
|
|
# If full verbosity requested, compute values at the final point.
|
|
if verbose >= Verbosity.FULLITER.value:
|
|
# Get Jacobian jac.
|
|
t_start = time.time()
|
|
if jacobian is None:
|
|
jac, n_res = jacobian_fd(residual, x, r, eps, n_res, bounds)
|
|
t_res += time.time() - t_start
|
|
else:
|
|
jac = jacobian(x, r)
|
|
t_jac += time.time() - t_start
|
|
n_jac += 1
|
|
|
|
# Get gradient, add to log.
|
|
grad, _ = norm.grad_hess(r, jac)
|
|
log = dataclasses.replace(log, residual=r, jacobian=jac, grad=grad)
|
|
|
|
trace.append(log)
|
|
if iter_callback is not None:
|
|
iter_callback(trace)
|
|
|
|
# Print final diagnostics.
|
|
if verbose > Verbosity.SILENT.value:
|
|
message = f'Terminated after {i} iterations: '
|
|
message += _STATUS_MESSAGE[status]
|
|
message += f' y: {yfinal:<.4g}, Residual evals: {n_res:d}'
|
|
if n_jac > 0:
|
|
message += f', Jacobian evals: {n_jac:d}'
|
|
print(message, file=output)
|
|
|
|
time_total = time.time() - t_start_total
|
|
if time_total > 0:
|
|
r_percent = 100 * t_res / time_total
|
|
time_scale = 1 if time_total > 1 else 1000
|
|
time_units = 's' if time_total > 1 else 'ms'
|
|
message = f'total time {time_scale * time_total:<.1f}{time_units}'
|
|
message += f' of which residual {r_percent:<.1f}%'
|
|
if t_jac > 0:
|
|
jac_percent = 100 * t_jac / time_total
|
|
message += f' Jacobian {jac_percent:<.1f}%'
|
|
print(message, file=output)
|
|
|
|
return x.reshape(x0.shape), trace
|
|
|
|
|
|
def jacobian_fd(
|
|
residual: Callable[[np.ndarray], np.ndarray],
|
|
x: np.ndarray,
|
|
r: np.ndarray,
|
|
eps: np.float64,
|
|
n_res: int,
|
|
bounds: Optional[List[np.ndarray]] = None,
|
|
) -> Tuple[np.ndarray, int]:
|
|
"""Finite-difference Jacobian of a residual function.
|
|
|
|
Args:
|
|
residual: vectorized function that returns the residual of a vector array.
|
|
x: point at which to evaluate the Jacobian.
|
|
r: residual at x.
|
|
eps: finite-difference step size.
|
|
n_res: number or residual evaluations so far.
|
|
bounds: optional pair of lower and upper bounds.
|
|
|
|
Returns:
|
|
jac: Jacobian of the residual at x.
|
|
n_res: updated number of residual evaluations (add x.size).
|
|
"""
|
|
n = x.size
|
|
if bounds is None:
|
|
eps_vec = eps * np.ones((n, 1))
|
|
else:
|
|
mid = 0.5 * (bounds[1] - bounds[0])
|
|
eps_vec = np.where(x > mid, -eps, eps)
|
|
eps_vec *= np.maximum(1.0, np.abs(x))
|
|
eps_vec = (eps_vec + x) - x
|
|
xh = x + np.diag(eps_vec.flatten())
|
|
rh = residual(xh)
|
|
jac = (rh - r) / eps_vec.T
|
|
return jac, n_res + n
|
|
|
|
|
|
def check_jacobian(
|
|
residual: Callable[[np.ndarray], np.ndarray],
|
|
x: np.ndarray,
|
|
r: np.ndarray,
|
|
jac: np.ndarray,
|
|
eps: np.float64,
|
|
n_res: int,
|
|
bounds: Optional[List[np.ndarray]] = None,
|
|
output: Optional[TextIO] = None,
|
|
name: Optional[str] = 'Jacobian',
|
|
) -> int:
|
|
"""Check user-provided Jacobian against internal finite-differencing.
|
|
|
|
Args:
|
|
residual: vectorized function that returns the residual of a vector array.
|
|
x: point at which the r and jac were evaluated.
|
|
r: residual at x.
|
|
jac: Jacobian at x.
|
|
eps: finite-difference step size.
|
|
n_res: number or residual evaluations so far.
|
|
bounds: optional pair of lower and upper bounds.
|
|
output: Optional file or StringIO to which to print messages.
|
|
name: Optional name of the function being tested.
|
|
|
|
Returns:
|
|
n_res: updated number of residual evaluations.
|
|
"""
|
|
jac_fd, n_res = jacobian_fd(residual, x, r, eps, n_res, bounds)
|
|
denom = np.abs(jac).sum() + np.abs(jac_fd).sum() + 1e-8
|
|
rel_diff = np.abs(jac - jac_fd) / denom
|
|
if np.any(rel_diff > 1e-5):
|
|
raise ValueError(
|
|
f'User-provided {name} does not match finite-differences '
|
|
'to a relative tolerance of 1e-5.'
|
|
)
|
|
print(f'User-provided {name} matches finite-differences.', file=output)
|
|
return n_res
|
|
|
|
|
|
def check_norm(
|
|
r: np.ndarray,
|
|
norm: Norm,
|
|
eps: np.float64,
|
|
output: Optional[TextIO] = None,
|
|
):
|
|
"""Check user-provided norm against internal finite-differencing.
|
|
|
|
Args:
|
|
r: residual vector.
|
|
norm: Norm function returning either the norm scalar or its gradient and
|
|
Gauss-Newton Hessian.
|
|
eps: finite-difference step size.
|
|
output: Optional file or StringIO to which to print messages.
|
|
"""
|
|
# Get norm(r) value and 1st, 2nd derivatives.
|
|
n = np.atleast_2d(norm.value(r)) # norm value as 1x1 array.
|
|
eye = np.eye(r.size) # Identity projection.
|
|
n_g, n_h = norm.grad_hess(r, eye) # Gradient and Hessian.
|
|
|
|
# Check that Hessian is symmetric.
|
|
if not np.allclose(n_h, n_h.T):
|
|
raise ValueError('User-provided norm Hessian is not symmetric.')
|
|
|
|
# Check that Hessian is positive-definite.
|
|
if np.any(np.linalg.eigvals(n_h) < 0):
|
|
h_min = np.min(np.linalg.eigvals(n_h))
|
|
raise ValueError(
|
|
'User-provided norm Hessian is not positive definite. '
|
|
f'Minimum eigenvalue is {h_min:<.4g}'
|
|
)
|
|
|
|
# Local function returning norm values (vectorized).
|
|
def norm_vec(v):
|
|
norms = [
|
|
np.atleast_2d(norm.value(v[:, i : i + 1])) for i in range(v.shape[1])
|
|
]
|
|
return np.hstack(norms)
|
|
|
|
# Check the norm gradient.
|
|
check_jacobian(norm_vec, r, n, n_g.T, eps, 0, None, output, 'norm gradient')
|
|
|
|
# Local function returning norm gradients (vectorized).
|
|
def grad_vec(v):
|
|
gradients = [
|
|
norm.grad_hess(v[:, i : i + 1], eye)[0] for i in range(v.shape[1])
|
|
]
|
|
return np.hstack(gradients)
|
|
|
|
# Check the norm Hessian.
|
|
check_jacobian(grad_vec, r, n_g, n_h, eps, 0, None, output, 'norm Hessian')
|