Source code for lbfgsb.linesearch

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Antoine COLLET

r"""
Implement the line search algorithm by Moré and Thuente (1994),
currently used for the L-BFGS-B algorithm.

The target of this line search algorithm is to find a step size :math:`\alpha`
that satisfies the strong Wolfe conditions

.. math::

    f(x + \alpha d) \leq f(x) + \alpha \mu g(x)^T d

and

.. math::

    |g(x + \alpha d)^T d| \leq \eta |g(x)^T d|.

Functions
^^^^^^^^^

.. autosummary::
   :toctree: _autosummary

    max_allowed_steplength
    line_search

References
----------
[1] Moré, J. J., & Thuente, D. J. (1994). Line search algorithms with guaranteed
    sufficient decrease.
"""

import logging
import warnings
from typing import Optional

import numpy as np
import scipy as sp
from packaging.version import Version
from scipy import __version__ as spversion

from lbfgsb._numba_helpers import njit
from lbfgsb.scalar_function import ScalarFunction
from lbfgsb.types import NDArrayFloat

_FLOAT64_MAX = np.finfo(np.float64).max


def _safe_sumsq(x: NDArrayFloat) -> float:
    """Return ``sum(x**2)`` without emitting overflow warnings.

    If the squared norm cannot be represented as a finite ``float64``, return
    ``np.inf``.

    Parameters
    ----------
    x : NDArrayFloat
        Input vector.

    Returns
    -------
    float
        Squared Euclidean norm, or ``np.inf`` if it cannot be represented
        finitely.
    """
    if x.size == 0:
        return 0.0

    if not np.all(np.isfinite(x)):
        return float("inf")

    max_abs = float(np.max(np.abs(x)))

    if max_abs == 0.0:
        return 0.0

    if not np.isfinite(max_abs):
        return float("inf")

    y = x / max_abs

    with np.errstate(over="ignore", invalid="ignore"):
        scaled_sum = float(np.dot(y, y))

    if not np.isfinite(scaled_sum) or scaled_sum <= 0.0:
        return float("inf")

    limit = np.sqrt(_FLOAT64_MAX / scaled_sum)

    if max_abs > limit:
        return float("inf")

    return float(max_abs * max_abs * scaled_sum)


def _safe_dot(a: NDArrayFloat, b: NDArrayFloat) -> float:
    """Return ``dot(a, b)`` while avoiding floating-point warnings.

    Parameters
    ----------
    a, b : NDArrayFloat
        Input vectors.

    Returns
    -------
    float
        Dot product if finite, otherwise ``np.inf``.
    """
    if not np.all(np.isfinite(a)) or not np.all(np.isfinite(b)):
        return float("inf")

    with np.errstate(over="ignore", invalid="ignore"):
        value = float(np.dot(a, b))

    if not np.isfinite(value):
        return float("inf")

    return value


def _trial_point(
    x0: NDArrayFloat,
    alpha: float,
    d: NDArrayFloat,
) -> Optional[NDArrayFloat]:
    """Return ``x0 + alpha * d`` if finite, otherwise ``None``."""
    if not np.isfinite(alpha):
        return None

    with np.errstate(over="ignore", invalid="ignore"):
        x = x0 + alpha * d

    if not np.all(np.isfinite(x)):
        return None

    return x


[docs] def max_allowed_steplength( x: NDArrayFloat, d: NDArrayFloat, lb: NDArrayFloat, ub: NDArrayFloat, max_steplength: float, n_iter: int, ) -> float: r""" Compute the largest admissible nonnegative step length. The returned step length ``alpha`` satisfies .. math:: lb \leq x + \alpha d \leq ub and .. math:: 0 \leq \alpha \leq \mathrm{max\_steplength}. Parameters ---------- x : NDArrayFloat Starting point. d : NDArrayFloat Search direction. lb : NDArrayFloat Lower bounds. ub : NDArrayFloat Upper bounds. max_steplength : float Maximum step length allowed by the caller. n_iter : int Current number of outer iterations. Returns ------- float Maximum admissible step length. References ---------- * R. H. Byrd, P. Lu and J. Nocedal. A Limited Memory Algorithm for Bound Constrained Optimization, (1995), SIAM Journal on Scientific and Statistical Computing, 16, 5, pp. 1190-1208. * C. Zhu, R. H. Byrd and J. Nocedal. L-BFGS-B: Algorithm 778: L-BFGS-B, FORTRAN routines for large scale bound constrained optimization (1997), ACM Transactions on Mathematical Software, 23, 4, pp. 550-560. * J. L. Morales and J. Nocedal. L-BFGS-B: Remark on Algorithm 778: L-BFGS-B, FORTRAN routines for large scale bound constrained optimization (2011), ACM Transactions on Mathematical Software, 38, 1. """ if n_iter == 0: return 1.0 if not np.isfinite(max_steplength) or max_steplength <= 0.0: return 0.0 if not np.all(np.isfinite(d)): return 0.0 mask = d != 0.0 if not np.any(mask): return float(max_steplength) with np.errstate(divide="ignore", invalid="ignore", over="ignore"): candidates = np.where( d[mask] > 0.0, (ub[mask] - x[mask]) / d[mask], (lb[mask] - x[mask]) / d[mask], ) candidates = candidates[np.isfinite(candidates)] candidates = candidates[candidates >= 0.0] if candidates.size == 0: return float(max_steplength) return float(min(max_steplength, float(np.min(candidates))))
@njit(cache=True) def max_allowed_steplength_numba( x: NDArrayFloat, d: NDArrayFloat, lb: NDArrayFloat, ub: NDArrayFloat, max_steplength: float, n_iter: int, ) -> float: """See :func:`max_allowed_steplength`.""" if n_iter == 0: return 1.0 if not np.isfinite(max_steplength) or max_steplength <= 0.0: return 0.0 alpha = max_steplength found = False n = x.size for i in range(n): di = d[i] if not np.isfinite(di): return 0.0 if di != 0.0: if di > 0.0: tmp = (ub[i] - x[i]) / di else: tmp = (lb[i] - x[i]) / di if np.isfinite(tmp) and tmp >= 0.0: if not found or tmp < alpha: alpha = tmp found = True return alpha