import warnings

import numpy as np
import pytest
from scipy.linalg import block_diag
from scipy.sparse import csc_array
from numpy.testing import (assert_array_almost_equal,
                           assert_array_less, assert_)
from scipy.optimize import (NonlinearConstraint,
                            LinearConstraint,
                            Bounds,
                            minimize,
                            BFGS,
                            SR1,
                            rosen)


class Maratos:
    """Problem 15.4 from Nocedal and Wright

    The following optimization problem:
        minimize 2*(x[0]**2 + x[1]**2 - 1) - x[0]
        Subject to: x[0]**2 + x[1]**2 - 1 = 0
    """

    def __init__(self, degrees=60, constr_jac=None, constr_hess=None):
        rads = degrees/180*np.pi
        self.x0 = [np.cos(rads), np.sin(rads)]
        self.x_opt = np.array([1.0, 0.0])
        self.constr_jac = constr_jac
        self.constr_hess = constr_hess
        self.bounds = None

    def fun(self, x):
        return 2*(x[0]**2 + x[1]**2 - 1) - x[0]

    def grad(self, x):
        return np.array([4*x[0]-1, 4*x[1]])

    def hess(self, x):
        return 4*np.eye(2)

    @property
    def constr(self):
        def fun(x):
            return x[0]**2 + x[1]**2

        if self.constr_jac is None:
            def jac(x):
                return [[2*x[0], 2*x[1]]]
        else:
            jac = self.constr_jac

        if self.constr_hess is None:
            def hess(x, v):
                return 2*v[0]*np.eye(2)
        else:
            hess = self.constr_hess

        return NonlinearConstraint(fun, 1, 1, jac, hess)


class MaratosTestArgs:
    """Problem 15.4 from Nocedal and Wright

    The following optimization problem:
        minimize 2*(x[0]**2 + x[1]**2 - 1) - x[0]
        Subject to: x[0]**2 + x[1]**2 - 1 = 0
    """

    def __init__(self, a, b, degrees=60, constr_jac=None, constr_hess=None):
        rads = degrees/180*np.pi
        self.x0 = [np.cos(rads), np.sin(rads)]
        self.x_opt = np.array([1.0, 0.0])
        self.constr_jac = constr_jac
        self.constr_hess = constr_hess
        self.a = a
        self.b = b
        self.bounds = None

    def _test_args(self, a, b):
        if self.a != a or self.b != b:
            raise ValueError()

    def fun(self, x, a, b):
        self._test_args(a, b)
        return 2*(x[0]**2 + x[1]**2 - 1) - x[0]

    def grad(self, x, a, b):
        self._test_args(a, b)
        return np.array([4*x[0]-1, 4*x[1]])

    def hess(self, x, a, b):
        self._test_args(a, b)
        return 4*np.eye(2)

    @property
    def constr(self):
        def fun(x):
            return x[0]**2 + x[1]**2

        if self.constr_jac is None:
            def jac(x):
                return [[4*x[0], 4*x[1]]]
        else:
            jac = self.constr_jac

        if self.constr_hess is None:
            def hess(x, v):
                return 2*v[0]*np.eye(2)
        else:
            hess = self.constr_hess

        return NonlinearConstraint(fun, 1, 1, jac, hess)


class MaratosGradInFunc:
    """Problem 15.4 from Nocedal and Wright

    The following optimization problem:
        minimize 2*(x[0]**2 + x[1]**2 - 1) - x[0]
        Subject to: x[0]**2 + x[1]**2 - 1 = 0
    """

    def __init__(self, degrees=60, constr_jac=None, constr_hess=None):
        rads = degrees/180*np.pi
        self.x0 = [np.cos(rads), np.sin(rads)]
        self.x_opt = np.array([1.0, 0.0])
        self.constr_jac = constr_jac
        self.constr_hess = constr_hess
        self.bounds = None

    def fun(self, x):
        return (2*(x[0]**2 + x[1]**2 - 1) - x[0],
                np.array([4*x[0]-1, 4*x[1]]))

    @property
    def grad(self):
        return True

    def hess(self, x):
        return 4*np.eye(2)

    @property
    def constr(self):
        def fun(x):
            return x[0]**2 + x[1]**2

        if self.constr_jac is None:
            def jac(x):
                return [[4*x[0], 4*x[1]]]
        else:
            jac = self.constr_jac

        if self.constr_hess is None:
            def hess(x, v):
                return 2*v[0]*np.eye(2)
        else:
            hess = self.constr_hess

        return NonlinearConstraint(fun, 1, 1, jac, hess)


class HyperbolicIneq:
    """Problem 15.1 from Nocedal and Wright

    The following optimization problem:
        minimize 1/2*(x[0] - 2)**2 + 1/2*(x[1] - 1/2)**2
        Subject to: 1/(x[0] + 1) - x[1] >= 1/4
                                   x[0] >= 0
                                   x[1] >= 0
    """
    def __init__(self, constr_jac=None, constr_hess=None):
        self.x0 = [0, 0]
        self.x_opt = [1.952823, 0.088659]
        self.constr_jac = constr_jac
        self.constr_hess = constr_hess
        self.bounds = Bounds(0, np.inf)

    def fun(self, x):
        return 1/2*(x[0] - 2)**2 + 1/2*(x[1] - 1/2)**2

    def grad(self, x):
        return [x[0] - 2, x[1] - 1/2]

    def hess(self, x):
        return np.eye(2)

    @property
    def constr(self):
        def fun(x):
            return 1/(x[0] + 1) - x[1]

        if self.constr_jac is None:
            def jac(x):
                return [[-1/(x[0] + 1)**2, -1]]
        else:
            jac = self.constr_jac

        if self.constr_hess is None:
            def hess(x, v):
                return 2*v[0]*np.array([[1/(x[0] + 1)**3, 0],
                                        [0, 0]])
        else:
            hess = self.constr_hess

        return NonlinearConstraint(fun, 0.25, np.inf, jac, hess)


class Rosenbrock:
    """Rosenbrock function.

    The following optimization problem:
        minimize sum(100.0*(x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0)
    """

    def __init__(self, n=2, random_state=0):
        rng = np.random.RandomState(random_state)
        self.x0 = rng.uniform(-1, 1, n)
        self.x_opt = np.ones(n)
        self.bounds = None

    def fun(self, x):
        x = np.asarray(x)
        r = np.sum(100.0 * (x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0,
                   axis=0)
        return r

    def grad(self, x):
        x = np.asarray(x)
        xm = x[1:-1]
        xm_m1 = x[:-2]
        xm_p1 = x[2:]
        der = np.zeros_like(x)
        der[1:-1] = (200 * (xm - xm_m1**2) -
                     400 * (xm_p1 - xm**2) * xm - 2 * (1 - xm))
        der[0] = -400 * x[0] * (x[1] - x[0]**2) - 2 * (1 - x[0])
        der[-1] = 200 * (x[-1] - x[-2]**2)
        return der

    def hess(self, x):
        x = np.atleast_1d(x)
        H = np.diag(-400 * x[:-1], 1) - np.diag(400 * x[:-1], -1)
        diagonal = np.zeros(len(x), dtype=x.dtype)
        diagonal[0] = 1200 * x[0]**2 - 400 * x[1] + 2
        diagonal[-1] = 200
        diagonal[1:-1] = 202 + 1200 * x[1:-1]**2 - 400 * x[2:]
        H = H + np.diag(diagonal)
        return H

    @property
    def constr(self):
        return ()


class IneqRosenbrock(Rosenbrock):
    """Rosenbrock subject to inequality constraints.

    The following optimization problem:
        minimize sum(100.0*(x[1] - x[0]**2)**2.0 + (1 - x[0])**2)
        subject to: x[0] + 2 x[1] <= 1

    Taken from matlab ``fmincon`` documentation.
    """
    def __init__(self, random_state=0):
        Rosenbrock.__init__(self, 2, random_state)
        self.x0 = [-1, -0.5]
        self.x_opt = [0.5022, 0.2489]
        self.bounds = None

    @property
    def constr(self):
        A = [[1, 2]]
        b = 1
        return LinearConstraint(A, -np.inf, b)


class BoundedRosenbrock(Rosenbrock):
    """Rosenbrock subject to inequality constraints.

    The following optimization problem:
        minimize sum(100.0*(x[1] - x[0]**2)**2.0 + (1 - x[0])**2)
        subject to:  -2 <= x[0] <= 0
                      0 <= x[1] <= 2

    Taken from matlab ``fmincon`` documentation.
    """
    def __init__(self, random_state=0):
        Rosenbrock.__init__(self, 2, random_state)
        self.x0 = [-0.2, 0.2]
        self.x_opt = None
        self.bounds = Bounds([-2, 0], [0, 2])


class EqIneqRosenbrock(Rosenbrock):
    """Rosenbrock subject to equality and inequality constraints.

    The following optimization problem:
        minimize sum(100.0*(x[1] - x[0]**2)**2.0 + (1 - x[0])**2)
        subject to: x[0] + 2 x[1] <= 1
                    2 x[0] + x[1] = 1

    Taken from matlab ``fimincon`` documentation.
    """
    def __init__(self, random_state=0):
        Rosenbrock.__init__(self, 2, random_state)
        self.x0 = [-1, -0.5]
        self.x_opt = [0.41494, 0.17011]
        self.bounds = None

    @property
    def constr(self):
        A_ineq = [[1, 2]]
        b_ineq = 1
        A_eq = [[2, 1]]
        b_eq = 1
        return (LinearConstraint(A_ineq, -np.inf, b_ineq),
                LinearConstraint(A_eq, b_eq, b_eq))


class Elec:
    """Distribution of electrons on a sphere.

    Problem no 2 from COPS collection [2]_. Find
    the equilibrium state distribution (of minimal
    potential) of the electrons positioned on a
    conducting sphere.

    References
    ----------
    .. [1] E. D. Dolan, J. J. Mor\'{e}, and T. S. Munson,
           "Benchmarking optimization software with COPS 3.0.",
            Argonne National Lab., Argonne, IL (US), 2004.
    """
    def __init__(self, n_electrons=200, random_state=0,
                 constr_jac=None, constr_hess=None):
        self.n_electrons = n_electrons
        self.rng = np.random.RandomState(random_state)
        # Initial Guess
        phi = self.rng.uniform(0, 2 * np.pi, self.n_electrons)
        theta = self.rng.uniform(-np.pi, np.pi, self.n_electrons)
        x = np.cos(theta) * np.cos(phi)
        y = np.cos(theta) * np.sin(phi)
        z = np.sin(theta)
        self.x0 = np.hstack((x, y, z))
        self.x_opt = None
        self.constr_jac = constr_jac
        self.constr_hess = constr_hess
        self.bounds = None

    def _get_cordinates(self, x):
        x_coord = x[:self.n_electrons]
        y_coord = x[self.n_electrons:2 * self.n_electrons]
        z_coord = x[2 * self.n_electrons:]
        return x_coord, y_coord, z_coord

    def _compute_coordinate_deltas(self, x):
        x_coord, y_coord, z_coord = self._get_cordinates(x)
        dx = x_coord[:, None] - x_coord
        dy = y_coord[:, None] - y_coord
        dz = z_coord[:, None] - z_coord
        return dx, dy, dz

    def fun(self, x):
        dx, dy, dz = self._compute_coordinate_deltas(x)
        with np.errstate(divide='ignore'):
            dm1 = (dx**2 + dy**2 + dz**2) ** -0.5
        dm1[np.diag_indices_from(dm1)] = 0
        return 0.5 * np.sum(dm1)

    def grad(self, x):
        dx, dy, dz = self._compute_coordinate_deltas(x)

        with np.errstate(divide='ignore'):
            dm3 = (dx**2 + dy**2 + dz**2) ** -1.5
        dm3[np.diag_indices_from(dm3)] = 0

        grad_x = -np.sum(dx * dm3, axis=1)
        grad_y = -np.sum(dy * dm3, axis=1)
        grad_z = -np.sum(dz * dm3, axis=1)

        return np.hstack((grad_x, grad_y, grad_z))

    def hess(self, x):
        dx, dy, dz = self._compute_coordinate_deltas(x)
        d = (dx**2 + dy**2 + dz**2) ** 0.5

        with np.errstate(divide='ignore'):
            dm3 = d ** -3
            dm5 = d ** -5

        i = np.arange(self.n_electrons)
        dm3[i, i] = 0
        dm5[i, i] = 0

        Hxx = dm3 - 3 * dx**2 * dm5
        Hxx[i, i] = -np.sum(Hxx, axis=1)

        Hxy = -3 * dx * dy * dm5
        Hxy[i, i] = -np.sum(Hxy, axis=1)

        Hxz = -3 * dx * dz * dm5
        Hxz[i, i] = -np.sum(Hxz, axis=1)

        Hyy = dm3 - 3 * dy**2 * dm5
        Hyy[i, i] = -np.sum(Hyy, axis=1)

        Hyz = -3 * dy * dz * dm5
        Hyz[i, i] = -np.sum(Hyz, axis=1)

        Hzz = dm3 - 3 * dz**2 * dm5
        Hzz[i, i] = -np.sum(Hzz, axis=1)

        H = np.vstack((
            np.hstack((Hxx, Hxy, Hxz)),
            np.hstack((Hxy, Hyy, Hyz)),
            np.hstack((Hxz, Hyz, Hzz))
        ))

        return H

    @property
    def constr(self):
        def fun(x):
            x_coord, y_coord, z_coord = self._get_cordinates(x)
            return x_coord**2 + y_coord**2 + z_coord**2 - 1

        if self.constr_jac is None:
            def jac(x):
                x_coord, y_coord, z_coord = self._get_cordinates(x)
                Jx = 2 * np.diag(x_coord)
                Jy = 2 * np.diag(y_coord)
                Jz = 2 * np.diag(z_coord)
                return csc_array(np.hstack((Jx, Jy, Jz)))
        else:
            jac = self.constr_jac

        if self.constr_hess is None:
            def hess(x, v):
                D = 2 * np.diag(v)
                return block_diag(D, D, D)
        else:
            hess = self.constr_hess

        return NonlinearConstraint(fun, -np.inf, 0, jac, hess)


class TestTrustRegionConstr:
    list_of_problems = [Maratos(),
                        Maratos(constr_hess='2-point'),
                        Maratos(constr_hess=SR1()),
                        Maratos(constr_jac='2-point', constr_hess=SR1()),
                        MaratosGradInFunc(),
                        HyperbolicIneq(),
                        HyperbolicIneq(constr_hess='3-point'),
                        HyperbolicIneq(constr_hess=BFGS()),
                        HyperbolicIneq(constr_jac='3-point',
                                       constr_hess=BFGS()),
                        Rosenbrock(),
                        IneqRosenbrock(),
                        EqIneqRosenbrock(),
                        BoundedRosenbrock(),
                        Elec(n_electrons=2),
                        Elec(n_electrons=2, constr_hess='2-point'),
                        Elec(n_electrons=2, constr_hess=SR1()),
                        Elec(n_electrons=2, constr_jac='3-point',
                             constr_hess=SR1())]

    @pytest.mark.parametrize('prob', list_of_problems)
    @pytest.mark.parametrize('grad', ('prob.grad', '3-point', False))
    @pytest.mark.parametrize('hess', ("prob.hess", '3-point', lambda: SR1(),
                                      lambda: BFGS(exception_strategy='damp_update'),
                                      lambda: BFGS(exception_strategy='skip_update')))
    def test_list_of_problems(self, prob, grad, hess):
        grad = prob.grad if grad == "prob.grad" else grad
        hess = hess() if callable(hess) else hess
        hess = prob.hess if hess == "prob.hess" else hess
        # Remove exceptions
        if (grad in {'2-point', '3-point', 'cs', False} and
                hess in {'2-point', '3-point', 'cs'}):
            pytest.skip("Numerical Hessian needs analytical gradient")
        if prob.grad is True and grad in {'3-point', False}:
            pytest.skip("prob.grad incompatible with grad in {'3-point', False}")
        sensitive = (isinstance(prob, BoundedRosenbrock) and grad == '3-point'
                     and isinstance(hess, BFGS))
        if sensitive:
            pytest.xfail("Seems sensitive to initial conditions w/ Accelerate")
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", "delta_grad == 0.0", UserWarning)
            result = minimize(prob.fun, prob.x0,
                              method='trust-constr',
                              jac=grad, hess=hess,
                              bounds=prob.bounds,
                              constraints=prob.constr)

        if prob.x_opt is not None:
            assert_array_almost_equal(result.x, prob.x_opt,
                                      decimal=5)
            # gtol
            if result.status == 1:
                assert_array_less(result.optimality, 1e-8)
        # xtol
        if result.status == 2:
            assert_array_less(result.tr_radius, 1e-8)

            if result.method == "tr_interior_point":
                assert_array_less(result.barrier_parameter, 1e-8)

        # check for max iter
        message = f"Invalid termination condition: {result.status}."
        assert result.status not in {0, 3}, message


    def test_default_jac_and_hess(self):
        def fun(x):
            return (x - 1) ** 2
        bounds = [(-2, 2)]
        res = minimize(fun, x0=[-1.5], bounds=bounds, method='trust-constr')
        assert_array_almost_equal(res.x, 1, decimal=5)

    def test_default_hess(self):
        def fun(x):
            return (x - 1) ** 2
        bounds = [(-2, 2)]
        res = minimize(fun, x0=[-1.5], bounds=bounds, method='trust-constr',
                       jac='2-point')
        assert_array_almost_equal(res.x, 1, decimal=5)

    def test_no_constraints(self):
        prob = Rosenbrock()
        result = minimize(prob.fun, prob.x0,
                          method='trust-constr',
                          jac=prob.grad, hess=prob.hess)
        result1 = minimize(prob.fun, prob.x0,
                           method='L-BFGS-B',
                           jac='2-point')

        result2 = minimize(prob.fun, prob.x0,
                           method='L-BFGS-B',
                           jac='3-point')
        assert_array_almost_equal(result.x, prob.x_opt, decimal=5)
        assert_array_almost_equal(result1.x, prob.x_opt, decimal=5)
        assert_array_almost_equal(result2.x, prob.x_opt, decimal=5)

    def test_hessp(self):
        prob = Maratos()

        def hessp(x, p):
            H = prob.hess(x)
            return H.dot(p)

        result = minimize(prob.fun, prob.x0,
                          method='trust-constr',
                          jac=prob.grad, hessp=hessp,
                          bounds=prob.bounds,
                          constraints=prob.constr)

        if prob.x_opt is not None:
            assert_array_almost_equal(result.x, prob.x_opt, decimal=2)

        # gtol
        if result.status == 1:
            assert_array_less(result.optimality, 1e-8)
        # xtol
        if result.status == 2:
            assert_array_less(result.tr_radius, 1e-8)

            if result.method == "tr_interior_point":
                assert_array_less(result.barrier_parameter, 1e-8)
        # max iter
        if result.status in (0, 3):
            raise RuntimeError("Invalid termination condition.")

    def test_args(self):
        prob = MaratosTestArgs("a", 234)

        result = minimize(prob.fun, prob.x0, ("a", 234),
                          method='trust-constr',
                          jac=prob.grad, hess=prob.hess,
                          bounds=prob.bounds,
                          constraints=prob.constr)

        if prob.x_opt is not None:
            assert_array_almost_equal(result.x, prob.x_opt, decimal=2)

        # gtol
        if result.status == 1:
            assert_array_less(result.optimality, 1e-8)
        # xtol
        if result.status == 2:
            assert_array_less(result.tr_radius, 1e-8)
            if result.method == "tr_interior_point":
                assert_array_less(result.barrier_parameter, 1e-8)
        # max iter
        if result.status in (0, 3):
            raise RuntimeError("Invalid termination condition.")

    def test_raise_exception(self):
        prob = Maratos()
        message = "Whenever the gradient is estimated via finite-differences"
        with pytest.raises(ValueError, match=message):
            minimize(prob.fun, prob.x0, method='trust-constr', jac='2-point',
                     hess='2-point', constraints=prob.constr)

    def test_issue_9044(self):
        # https://github.com/scipy/scipy/issues/9044
        # Test the returned `OptimizeResult` contains keys consistent with
        # other solvers.

        def callback(x, info):
            assert_('nit' in info)
            assert_('niter' in info)

        result = minimize(lambda x: x**2, [0], jac=lambda x: 2*x,
                          hess=lambda x: 2, callback=callback,
                          method='trust-constr')
        assert_(result.get('success'))
        assert_(result.get('nit', -1) == 1)

        # Also check existence of the 'niter' attribute, for backward
        # compatibility
        assert_(result.get('niter', -1) == 1)

    def test_issue_15093(self):
        # scipy docs define bounds as inclusive, so it shouldn't be
        # an issue to set x0 on the bounds even if keep_feasible is
        # True. Previously, trust-constr would treat bounds as
        # exclusive.

        x0 = np.array([0., 0.5])

        def obj(x):
            x1 = x[0]
            x2 = x[1]
            return x1 ** 2 + x2 ** 2

        bounds = Bounds(np.array([0., 0.]), np.array([1., 1.]),
                        keep_feasible=True)

        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", "delta_grad == 0.0", UserWarning)
            result = minimize(
                method='trust-constr',
                fun=obj,
                x0=x0,
                bounds=bounds)

        assert result['success']

class TestEmptyConstraint:
    """
    Here we minimize x^2+y^2 subject to x^2-y^2>1.
    The actual minimum is at (0, 0) which fails the constraint.
    Therefore we will find a minimum on the boundary at (+/-1, 0).

    When minimizing on the boundary, optimize uses a set of
    constraints that removes the constraint that sets that
    boundary.  In our case, there's only one constraint, so
    the result is an empty constraint.

    This tests that the empty constraint works.
    """
    def test_empty_constraint(self):

        def function(x):
            return x[0]**2 + x[1]**2

        def functionjacobian(x):
            return np.array([2.*x[0], 2.*x[1]])

        def functionhvp(x, v):
            return 2.*v

        def constraint(x):
            return np.array([x[0]**2 - x[1]**2])

        def constraintjacobian(x):
            return np.array([[2*x[0], -2*x[1]]])

        def constraintlcoh(x, v):
            return np.array([[2., 0.], [0., -2.]]) * v[0]

        constraint = NonlinearConstraint(constraint, 1., np.inf,
                                         constraintjacobian, constraintlcoh)

        startpoint = [1., 2.]

        bounds = Bounds([-np.inf, -np.inf], [np.inf, np.inf])

        result = minimize(
          function,
          startpoint,
          method='trust-constr',
          jac=functionjacobian,
          hessp=functionhvp,
          constraints=[constraint],
          bounds=bounds,
        )

        assert_array_almost_equal(abs(result.x), np.array([1, 0]), decimal=4)


def test_bug_11886():
    def opt(x):
        return x[0]**2+x[1]**2

    with warnings.catch_warnings():
        warnings.simplefilter("ignore", PendingDeprecationWarning)
        A = np.matrix(np.diag([1, 1]))
    lin_cons = LinearConstraint(A, -1, np.inf)
    # just checking that there are no errors
    minimize(opt, 2*[1], constraints = lin_cons)


def test_gh11649():
    # trust - constr error when attempting to keep bound constrained solutions
    # feasible. Algorithm attempts to go outside bounds when evaluating finite
    # differences. (don't give objective an analytic gradient)
    bnds = Bounds(lb=[-1, -1], ub=[1, 1], keep_feasible=True)

    def assert_inbounds(x):
        assert np.all(x >= bnds.lb)
        assert np.all(x <= bnds.ub)

    def obj(x):
        assert_inbounds(x)
        return np.exp(x[0])*(4*x[0]**2 + 2*x[1]**2 + 4*x[0]*x[1] + 2*x[1] + 1)

    def nce(x):
        assert_inbounds(x)
        return x[0]**2 + x[1]

    def nce_jac(x):
        return np.array([2*x[0], 1])

    def nci(x):
        assert_inbounds(x)
        return x[0]*x[1]

    x0 = np.array((0.99, -0.99))
    nlcs = [NonlinearConstraint(nci, -10, np.inf),
            NonlinearConstraint(nce, 1, 1, jac=nce_jac)]

    res = minimize(fun=obj, x0=x0, method='trust-constr',
                   bounds=bnds, constraints=nlcs)
    assert_inbounds(res.x)
    assert nlcs[0].lb < nlcs[0].fun(res.x) < nlcs[0].ub


def test_gh24424():
    def calculate_intermediates(a, b, c, data):
        result = np.empty(
            shape=len(data),
            dtype=float
        )
        result[0] = a / (1 - b - c)
        for i, d in enumerate(data[:-1], 1):
            result[i] = a + c * result[i - 1] + b * d
        return result

    def calculate_jacobian_intermediates(log_a, b, c, data, intermediates):
        a = np.exp(log_a)

        result = np.empty(
            shape=(len(data), 3),
            dtype=float
        )
        result[0, 0] = a / (1 - b - c)
        result[0, 1] = a / (1 - b - c) ** 2
        result[0, 2] = a / (1 - b - c) ** 2

        for i in range(1, len(data)):
            previous_jacobian_term = c * result[i - 1]

            result[i, 0] = a + previous_jacobian_term[0]
            result[i, 1] = data[i - 1] + previous_jacobian_term[1]
            result[i, 2] = intermediates[i - 1] + previous_jacobian_term[2]

        return result

    def objective_and_jacobian(theta, data):
        log_a, b, c = theta

        assert 0 <= b <= 1, "Bounds was violated for variable `a`."
        assert 0 <= c <= 1, "Bounds was violated for variable `b`."
        assert b + c < 1, "Constraint was violated."

        a = np.exp(log_a)

        intermediates = calculate_intermediates(
            a=a,
            b=b,
            c=c,
            data=data
        )
        result = np.sum(
            np.log(intermediates) + data / intermediates
        )

        jacobian_intermediates = calculate_jacobian_intermediates(
            log_a=log_a,
            b=b,
            c=c,
            data=data,
            intermediates=intermediates
        )
        jacobian = \
            (1 / intermediates - data / intermediates ** 2) @ jacobian_intermediates

        return result, jacobian

    data = np.array([
        2.9934552262644166e-05, 1.83404750498639e-05, 3.8762287534860667e-05,
        5.4537741557086936e-06, 1.8971702025284017e-05, 5.327721423321283e-06,
        7.975416195706544e-06, 1.1382485956213557e-05, 3.9576073822143895e-05,
        1.6686716514067753e-05, 2.521966334088718e-05, 1.2203016286995732e-05,
        6.104844206226364e-06, 3.656990591660379e-05, 7.50740799602968e-06,
        5.1787715626447475e-05, 4.068430728392944e-05, 1.6425909413963983e-06,
        2.323453810347028e-05, 3.2537707515840034e-05, 1.0199717988183908e-05,
        9.011651369071027e-06, 9.423474666676454e-06, 5.080061504315627e-06,
        2.41505514167834e-05, 2.513422432759034e-05, 2.0642198121990002e-05,
        8.321874187176471e-06, 1.019170126320171e-05, 2.597532020137934e-07,
        3.908918145517398e-05, 3.165502329497437e-05, 3.902338742601731e-06,
        1.9642839599876334e-05, 1.4202292239184809e-05, 1.6770752189396043e-05,
        6.213979916610612e-06, 3.171396103300187e-05, 9.934130727121179e-06,
        3.0358455907343625e-05, 6.224043027231856e-07, 1.063405415951252e-06,
        1.6308810826809896e-05, 7.867464317695382e-06, 1.4108106007697538e-05,
        1.8722709625238567e-05, 1.3198437078692119e-05, 2.221667555167261e-06,
        6.256595702046974e-05, 2.6361829576605727e-05, 1.7374401653652944e-05,
        5.183207198861357e-06, 1.0048728069764142e-05, 2.3198968737030035e-06,
        7.439480755759099e-06, 1.2298059429320431e-05, 2.0185774097335823e-05,
        8.014211503092624e-06, 9.305654715149352e-06, 3.60136657702133e-05,
        2.32149271461897e-05, 1.8652336119941976e-05, 3.473510850318922e-05,
        1.0194877213172668e-05, 2.2083922152696592e-05, 1.1766282835177292e-05,
        1.7086225762873947e-05, 6.466499880883932e-05, 9.234869650567825e-06,
        1.9427199320844276e-05, 2.8241613272189955e-05, 5.378122279928882e-05,
        2.604144856548882e-05, 1.9454208137286557e-05, 4.287312368904257e-05,
        1.57187091820659e-05, 1.2484808801911742e-05, 1.7167397579819053e-05,
        0.0, 2.0252462281186e-05, 1.4774599170124377e-05,
        0.0, 3.0064855641072096e-05, 1.1951667450097332e-05,
        2.6717918042680187e-05, 2.2491563831828074e-05, 1.4748949757360303e-05,
        9.422793251613177e-05, 8.668457255209887e-05, 2.3355215071734614e-05,
        4.4706397982269585e-05, 2.7992021752856294e-05, 7.694104461408618e-05,
        4.020190100203822e-05, 4.0915164084390224e-05, 7.174183343729235e-05,
        7.103034737172072e-05, 8.224690780946857e-05, 0.00012873982383790664,
        4.995816817323732e-05, 0.0, 5.921437092197502e-05,
        1.544436592616776e-05, 5.8810138143918906e-05, 1.9400221788675188e-05,
        4.5630225293291145e-05, 3.204082932872853e-05, 2.2445753506172103e-05,
        4.2075935382479554e-05, 2.4598027012525572e-05, 5.7410672937657564e-05,
        7.502234318198545e-05, 7.592571131932753e-05, 6.356522172494383e-06,
        5.386009659469673e-06, 2.584466503185e-05, 2.5724698395300155e-05,
        5.205615091351065e-05, 7.859446356765305e-05, 7.273145118589368e-06,
        2.314487591559627e-05, 3.69477147738378e-05, 2.1830541071086155e-05,
        7.511731105500895e-05, 1.5450999502759258e-05, 0.0,
        1.582211718453788e-05, 2.702016660887833e-05, 2.3641331381091936e-05,
        1.7579054910909477e-05, 6.383358007440582e-05, 1.925774054598795e-05,
        2.6648621120684558e-05, 1.2325662981013882e-05, 1.575955730688859e-05,
        7.021212451816662e-05, 1.5772697293897e-05, 1.7655937683952653e-05,
        2.5284532609827995e-06, 3.106257322767402e-05, 7.668118254072083e-05,
        4.684839412305774e-05, 2.5356641538859304e-05, 3.105206100981116e-05,
        2.7265881197510174e-05, 1.3920984856376055e-05, 7.08918619393585e-06,
        4.858002948371407e-05, 1.7633679540206005e-05, 3.9984786626623e-06,
        1.2150737261530062e-05, 1.4932495278445555e-05, 1.4907183322390042e-05,
        1.4608977266060503e-05, 1.2850307928856668e-05, 8.004737075371617e-06,
        3.620174626132346e-06, 1.5131513705442799e-05, 3.098052405312182e-06,
        1.0007362202939136e-05, 2.4049828598531798e-05, 1.6466966920680003e-05,
        1.7180612084121284e-05, 2.1266769642355435e-05, 1.4784179724678913e-05,
        9.732346693107168e-06, 9.033960719308952e-06, 1.3459809558545056e-05
    ])

    minimize(
        fun=objective_and_jacobian,
        x0=[
            np.log(np.mean(data)),
            0.45,
            0.3
        ],
        args=(
            data
        ),
        method='trust-constr',
        jac=True,
        bounds=Bounds(
            lb=[-np.inf, 0.0, 0.0],
            ub=[np.inf, 1.0, 1.0],
            keep_feasible=True
        ),
        constraints=LinearConstraint(
            A=[0, 1, 1],
            ub=0.999999,
            keep_feasible=True
        )
    )


def test_gh20665_too_many_constraints():
    # gh-20665 reports a confusing error message when there are more equality
    # constraints than variables. Check that the error message is improved.
    message = "...more equality constraints than independent variables..."
    with pytest.raises(ValueError, match=message):
        x0 = np.ones((2,))
        A_eq, b_eq = np.arange(6).reshape((3, 2)), np.ones((3,))
        g = NonlinearConstraint(lambda x:  A_eq @ x, lb=b_eq, ub=b_eq)
        minimize(rosen, x0, method='trust-constr', constraints=[g])
    # no error with `SVDFactorization`
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", UserWarning)
        minimize(rosen, x0, method='trust-constr', constraints=[g],
                 options={'factorization_method': 'SVDFactorization'})

def test_issue_18882():
    def lsf(u):
        u1, u2 = u
        a, b = [3.0, 4.0]
        return 1.0 + u1**2 / a**2 - u2**2 / b**2

    def of(u):
        return np.sum(u**2)

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", "delta_grad == 0.0", UserWarning)
        warnings.filterwarnings("ignore", "Singular Jacobian matrix.", UserWarning)
        res = minimize(
            of,
            [0.0, 0.0],
            method="trust-constr",
            constraints=NonlinearConstraint(lsf, 0, 0),
        )
    assert (not res.success) and (res.constr_violation > 1e-8)

class TestBoundedNelderMead:

    @pytest.mark.parametrize('bounds, x_opt',
                             [(Bounds(-np.inf, np.inf), Rosenbrock().x_opt),
                              (Bounds(-np.inf, -0.8), [-0.8, -0.8]),
                              (Bounds(3.0, np.inf), [3.0, 9.0]),
                              (Bounds([3.0, 1.0], [4.0, 5.0]), [3., 5.]),
                              ])
    def test_rosen_brock_with_bounds(self, bounds, x_opt):
        prob = Rosenbrock()
        with warnings.catch_warnings():
            msg = "Initial guess is not within the specified bounds"
            warnings.filterwarnings("ignore", msg, UserWarning)
            result = minimize(prob.fun, [-10, -10],
                              method='Nelder-Mead',
                              bounds=bounds)
            assert np.less_equal(bounds.lb, result.x).all()
            assert np.less_equal(result.x, bounds.ub).all()
            assert np.allclose(prob.fun(result.x), result.fun)
            assert np.allclose(result.x, x_opt, atol=1.e-3)

    def test_equal_all_bounds(self):
        prob = Rosenbrock()
        bounds = Bounds([4.0, 5.0], [4.0, 5.0])
        with warnings.catch_warnings():
            warnings.filterwarnings(
                "ignore",
                "Initial guess is not within the specified bounds",
                UserWarning)
            result = minimize(prob.fun, [-10, 8],
                              method='Nelder-Mead',
                              bounds=bounds)
            assert np.allclose(result.x, [4.0, 5.0])

    def test_equal_one_bounds(self):
        prob = Rosenbrock()
        bounds = Bounds([4.0, 5.0], [4.0, 20.0])
        with warnings.catch_warnings():
            warnings.filterwarnings(
                "ignore",
                "Initial guess is not within the specified bounds",
                UserWarning)
            result = minimize(prob.fun, [-10, 8],
                              method='Nelder-Mead',
                              bounds=bounds)
            assert np.allclose(result.x, [4.0, 16.0])

    def test_invalid_bounds(self):
        prob = Rosenbrock()
        message = 'An upper bound is less than the corresponding lower bound.'
        with pytest.raises(ValueError, match=message):
            bounds = Bounds([-np.inf, 1.0], [4.0, -5.0])
            minimize(prob.fun, [-10, 3],
                     method='Nelder-Mead',
                     bounds=bounds)

    @pytest.mark.xfail(reason="Failing on Azure Linux and macOS builds, "
                              "see gh-13846")
    def test_outside_bounds_warning(self):
        prob = Rosenbrock()
        message = "Initial guess is not within the specified bounds"
        with pytest.warns(UserWarning, match=message):
            bounds = Bounds([-np.inf, 1.0], [4.0, 5.0])
            minimize(prob.fun, [-10, 8],
                     method='Nelder-Mead',
                     bounds=bounds)
