import logging
import warnings

import numpy as np
import time
from multiprocessing import Pool
from numpy.testing import assert_allclose
import pytest
from pytest import raises as assert_raises, warns
from scipy.optimize import (shgo, Bounds, minimize_scalar, minimize, rosen,
                            rosen_der, rosen_hess, NonlinearConstraint, OptimizeWarning)
from scipy.optimize._constraints import new_constraint_to_old
from scipy.optimize._shgo import SHGO
from scipy.optimize.tests.test_minimize_constrained import MaratosTestArgs


class StructTestFunction:
    def __init__(self, bounds, expected_x, expected_fun=None,
                 expected_xl=None, expected_funl=None):
        self.bounds = bounds
        self.expected_x = expected_x
        self.expected_fun = expected_fun
        self.expected_xl = expected_xl
        self.expected_funl = expected_funl


def wrap_constraints(g):
    cons = []
    if g is not None:
        if not isinstance(g, tuple | list):
            g = (g,)
        else:
            pass
        for g in g:
            cons.append({'type': 'ineq',
                         'fun': g})
        cons = tuple(cons)
    else:
        cons = None
    return cons


class StructTest1(StructTestFunction):
    def f(self, x):
        return x[0] ** 2 + x[1] ** 2

    def g(x):
        return -(np.sum(x, axis=0) - 6.0)

    cons = wrap_constraints(g)


test1_1 = StructTest1(bounds=[(-1, 6), (-1, 6)],
                      expected_x=[0, 0])
test1_2 = StructTest1(bounds=[(0, 1), (0, 1)],
                      expected_x=[0, 0])
test1_3 = StructTest1(bounds=[(None, None), (None, None)],
                      expected_x=[0, 0])


class StructTest2(StructTestFunction):
    """
    Scalar function with several minima to test all minimiser retrievals
    """

    def f(self, x):
        return (x - 30) * np.sin(x)

    def g(x):
        return 58 - np.sum(x, axis=0)

    cons = wrap_constraints(g)


test2_1 = StructTest2(bounds=[(0, 60)],
                      expected_x=[1.53567906],
                      expected_fun=-28.44677132,
                      # Important: test that funl return is in the correct
                      # order
                      expected_xl=np.array([[1.53567906],
                                            [55.01782167],
                                            [7.80894889],
                                            [48.74797493],
                                            [14.07445705],
                                            [42.4913859],
                                            [20.31743841],
                                            [36.28607535],
                                            [26.43039605],
                                            [30.76371366]]),

                      expected_funl=np.array([-28.44677132, -24.99785984,
                                              -22.16855376, -18.72136195,
                                              -15.89423937, -12.45154942,
                                              -9.63133158, -6.20801301,
                                              -3.43727232, -0.46353338])
                      )

test2_2 = StructTest2(bounds=[(0, 4.5)],
                      expected_x=[1.53567906],
                      expected_fun=[-28.44677132],
                      expected_xl=np.array([[1.53567906]]),
                      expected_funl=np.array([-28.44677132])
                      )


class StructTest3(StructTestFunction):
    """
    Hock and Schittkowski 18 problem (HS18). Hoch and Schittkowski (1981)
    http://www.ai7.uni-bayreuth.de/test_problem_coll.pdf
    Minimize: f = 0.01 * (x_1)**2 + (x_2)**2

    Subject to: x_1 * x_2 - 25.0 >= 0,
                (x_1)**2 + (x_2)**2 - 25.0 >= 0,
                2 <= x_1 <= 50,
                0 <= x_2 <= 50.

    Approx. Answer:
        f([(250)**0.5 , (2.5)**0.5]) = 5.0


    """

    # amended to test vectorisation of constraints
    def f(self, x):
        return 0.01 * (x[0]) ** 2 + (x[1]) ** 2

    def g1(x):
        return x[0] * x[1] - 25.0

    def g2(x):
        return x[0] ** 2 + x[1] ** 2 - 25.0

    # g = (g1, g2)
    # cons = wrap_constraints(g)

    def g(x):
        return x[0] * x[1] - 25.0, x[0] ** 2 + x[1] ** 2 - 25.0

    # this checks that shgo can be sent new-style constraints
    __nlc = NonlinearConstraint(g, 0, np.inf)
    cons = (__nlc,)

test3_1 = StructTest3(bounds=[(2, 50), (0, 50)],
                      expected_x=[250 ** 0.5, 2.5 ** 0.5],
                      expected_fun=5.0
                      )


class StructTest4(StructTestFunction):
    """
    Hock and Schittkowski 11 problem (HS11). Hoch and Schittkowski (1981)

    NOTE: Did not find in original reference to HS collection, refer to
          Henderson (2015) problem 7 instead. 02.03.2016
    """

    def f(self, x):
        return ((x[0] - 10) ** 2 + 5 * (x[1] - 12) ** 2 + x[2] ** 4
                + 3 * (x[3] - 11) ** 2 + 10 * x[4] ** 6 + 7 * x[5] ** 2 + x[
                    6] ** 4
                - 4 * x[5] * x[6] - 10 * x[5] - 8 * x[6]
                )

    def g1(x):
        return -(2 * x[0] ** 2 + 3 * x[1] ** 4 + x[2] + 4 * x[3] ** 2
                 + 5 * x[4] - 127)

    def g2(x):
        return -(7 * x[0] + 3 * x[1] + 10 * x[2] ** 2 + x[3] - x[4] - 282.0)

    def g3(x):
        return -(23 * x[0] + x[1] ** 2 + 6 * x[5] ** 2 - 8 * x[6] - 196)

    def g4(x):
        return -(4 * x[0] ** 2 + x[1] ** 2 - 3 * x[0] * x[1] + 2 * x[2] ** 2
                 + 5 * x[5] - 11 * x[6])

    g = (g1, g2, g3, g4)

    cons = wrap_constraints(g)


test4_1 = StructTest4(bounds=[(-10, 10), ] * 7,
                      expected_x=[2.330499, 1.951372, -0.4775414,
                                  4.365726, -0.6244870, 1.038131, 1.594227],
                      expected_fun=680.6300573
                      )


class StructTest5(StructTestFunction):
    def f(self, x):
        return (
            -(x[1] + 47.0)*np.sin(np.sqrt(abs(x[0]/2.0 + (x[1] + 47.0))))
            - x[0]*np.sin(np.sqrt(abs(x[0] - (x[1] + 47.0))))
        )

    g = None
    cons = wrap_constraints(g)


test5_1 = StructTest5(bounds=[(-512, 512), (-512, 512)],
                      expected_fun=[-959.64066272085051],
                      expected_x=[512., 404.23180542])


class StructTestLJ(StructTestFunction):
    """
    LennardJones objective function. Used to test symmetry constraints
    settings.
    """

    def f(self, x, *args):
        print(f'x = {x}')
        self.N = args[0]
        k = int(self.N / 3)
        s = 0.0

        for i in range(k - 1):
            for j in range(i + 1, k):
                a = 3 * i
                b = 3 * j
                xd = x[a] - x[b]
                yd = x[a + 1] - x[b + 1]
                zd = x[a + 2] - x[b + 2]
                ed = xd * xd + yd * yd + zd * zd
                ud = ed * ed * ed
                if ed > 0.0:
                    s += (1.0 / ud - 2.0) / ud

        return s

    g = None
    cons = wrap_constraints(g)


N = 6
boundsLJ = list(zip([-4.0] * 6, [4.0] * 6))

testLJ = StructTestLJ(bounds=boundsLJ,
                      expected_fun=[-1.0],
                      expected_x=None,
                      # expected_x=[-2.71247337e-08,
                      #            -2.71247337e-08,
                      #            -2.50000222e+00,
                      #            -2.71247337e-08,
                      #            -2.71247337e-08,
                      #            -1.50000222e+00]
                      )


class StructTestS(StructTestFunction):
    def f(self, x):
        return ((x[0] - 0.5) ** 2 + (x[1] - 0.5) ** 2
                + (x[2] - 0.5) ** 2 + (x[3] - 0.5) ** 2)

    g = None
    cons = wrap_constraints(g)


test_s = StructTestS(bounds=[(0, 2.0), ] * 4,
                     expected_fun=0.0,
                     expected_x=np.ones(4) - 0.5
                     )


class StructTestTable(StructTestFunction):
    def f(self, x):
        if x[0] == 3.0 and x[1] == 3.0:
            return 50
        else:
            return 100

    g = None
    cons = wrap_constraints(g)


test_table = StructTestTable(bounds=[(-10, 10), (-10, 10)],
                             expected_fun=[50],
                             expected_x=[3.0, 3.0])


class StructTestInfeasible(StructTestFunction):
    """
    Test function with no feasible domain.
    """

    def f(self, x, *args):
        return x[0] ** 2 + x[1] ** 2

    def g1(x):
        return x[0] + x[1] - 1

    def g2(x):
        return -(x[0] + x[1] - 1)

    def g3(x):
        return -x[0] + x[1] - 1

    def g4(x):
        return -(-x[0] + x[1] - 1)

    g = (g1, g2, g3, g4)
    cons = wrap_constraints(g)


test_infeasible = StructTestInfeasible(bounds=[(2, 50), (-1, 1)],
                                       expected_fun=None,
                                       expected_x=None
                                       )


@pytest.mark.skip("Not a test")
def run_test(test, args=(), test_atol=1e-5, n=100, iters=None,
             callback=None, minimizer_kwargs=None, options=None,
             sampling_method='sobol', workers=1):
    res = shgo(test.f, test.bounds, args=args, constraints=test.cons,
               n=n, iters=iters, callback=callback,
               minimizer_kwargs=minimizer_kwargs, options=options,
               sampling_method=sampling_method, workers=workers)

    print(f'res = {res}')
    logging.info(f'res = {res}')
    if test.expected_x is not None:
        np.testing.assert_allclose(res.x, test.expected_x,
                                   rtol=test_atol,
                                   atol=test_atol)

    # (Optional tests)
    if test.expected_fun is not None:
        np.testing.assert_allclose(res.fun,
                                   test.expected_fun,
                                   atol=test_atol)

    if test.expected_xl is not None:
        np.testing.assert_allclose(res.xl,
                                   test.expected_xl,
                                   atol=test_atol)

    if test.expected_funl is not None:
        np.testing.assert_allclose(res.funl,
                                   test.expected_funl,
                                   atol=test_atol)
    return


# Base test functions:
class TestShgoSobolTestFunctions:
    """
    Global optimisation tests with Sobol sampling:
    """

    # Sobol algorithm
    def test_f1_1_sobol(self):
        """Multivariate test function 1:
        x[0]**2 + x[1]**2 with bounds=[(-1, 6), (-1, 6)]"""
        run_test(test1_1)

    def test_f1_2_sobol(self):
        """Multivariate test function 1:
         x[0]**2 + x[1]**2 with bounds=[(0, 1), (0, 1)]"""
        run_test(test1_2)

    def test_f1_3_sobol(self):
        """Multivariate test function 1:
        x[0]**2 + x[1]**2 with bounds=[(None, None),(None, None)]"""
        options = {'disp': True}
        run_test(test1_3, options=options)

    def test_f2_1_sobol(self):
        """Univariate test function on
        f(x) = (x - 30) * sin(x) with bounds=[(0, 60)]"""
        run_test(test2_1)

    def test_f2_2_sobol(self):
        """Univariate test function on
        f(x) = (x - 30) * sin(x) bounds=[(0, 4.5)]"""
        run_test(test2_2)

    def test_f3_sobol(self):
        """NLP: Hock and Schittkowski problem 18"""
        run_test(test3_1)

    @pytest.mark.slow
    def test_f4_sobol(self):
        """NLP: (High dimensional) Hock and Schittkowski 11 problem (HS11)"""
        options = {'infty_constraints': False}
        # run_test(test4_1, n=990, options=options)
        run_test(test4_1, n=990 * 2, options=options)

    def test_f5_1_sobol(self):
        """NLP: Eggholder, multimodal"""
        # run_test(test5_1, n=30)
        run_test(test5_1, n=60)

    def test_f5_2_sobol(self):
        """NLP: Eggholder, multimodal"""
        # run_test(test5_1, n=60, iters=5)
        run_test(test5_1, n=60, iters=5)

        # def test_t911(self):
        #    """1D tabletop function"""
        #    run_test(test11_1)


class TestShgoSimplicialTestFunctions:
    """
    Global optimisation tests with Simplicial sampling:
    """

    def test_f1_1_simplicial(self):
        """Multivariate test function 1:
        x[0]**2 + x[1]**2 with bounds=[(-1, 6), (-1, 6)]"""
        run_test(test1_1, n=1, sampling_method='simplicial')

    def test_f1_2_simplicial(self):
        """Multivariate test function 1:
        x[0]**2 + x[1]**2 with bounds=[(0, 1), (0, 1)]"""
        run_test(test1_2, n=1, sampling_method='simplicial')

    def test_f1_3_simplicial(self):
        """Multivariate test function 1: x[0]**2 + x[1]**2
        with bounds=[(None, None),(None, None)]"""
        run_test(test1_3, n=5, sampling_method='simplicial')

    def test_f2_1_simplicial(self):
        """Univariate test function on
        f(x) = (x - 30) * sin(x) with bounds=[(0, 60)]"""
        options = {'minimize_every_iter': False}
        run_test(test2_1, n=200, iters=7, options=options,
                 sampling_method='simplicial')

    def test_f2_2_simplicial(self):
        """Univariate test function on
        f(x) = (x - 30) * sin(x) bounds=[(0, 4.5)]"""
        run_test(test2_2, n=1, sampling_method='simplicial')

    def test_f3_simplicial(self):
        """NLP: Hock and Schittkowski problem 18"""
        run_test(test3_1, n=1, sampling_method='simplicial')

    @pytest.mark.slow
    def test_f4_simplicial(self):
        """NLP: (High dimensional) Hock and Schittkowski 11 problem (HS11)"""
        run_test(test4_1, n=1, sampling_method='simplicial')

    def test_lj_symmetry_old(self):
        """LJ: Symmetry-constrained test function"""
        options = {'symmetry': True,
                   'disp': True}
        args = (6,)  # Number of atoms
        run_test(testLJ, args=args, n=300,
                 options=options, iters=1,
                 sampling_method='simplicial')

    def test_f5_1_lj_symmetry(self):
        """LJ: Symmetry constrained test function"""
        options = {'symmetry': [0, ] * 6,
                   'disp': True}
        args = (6,)  # No. of atoms

        run_test(testLJ, args=args, n=300,
                 options=options, iters=1,
                 sampling_method='simplicial')

    def test_f5_2_cons_symmetry(self):
        """Symmetry constrained test function"""
        options = {'symmetry': [0, 0],
                   'disp': True}

        run_test(test1_1, n=200,
                 options=options, iters=1,
                 sampling_method='simplicial')

    @pytest.mark.fail_slow(10)
    def test_f5_3_cons_symmetry(self):
        """Asymmetrically constrained test function"""
        options = {'symmetry': [0, 0, 0, 3],
                   'disp': True}

        run_test(test_s, n=10000,
                 options=options,
                 iters=1,
                 sampling_method='simplicial')

    @pytest.mark.skip("Not a test")
    def test_f0_min_variance(self):
        """Return a minimum on a perfectly symmetric problem, based on
            gh10429"""
        avg = 0.5  # Given average value of x
        cons = {'type': 'eq', 'fun': lambda x: np.mean(x) - avg}

        # Minimize the variance of x under the given constraint
        res = shgo(np.var, bounds=6 * [(0, 1)], constraints=cons)
        assert res.success
        assert_allclose(res.fun, 0, atol=1e-15)
        assert_allclose(res.x, 0.5)

    @pytest.mark.skip("Not a test")
    def test_f0_min_variance_1D(self):
        """Return a minimum on a perfectly symmetric 1D problem, based on
            gh10538"""

        def fun(x):
            return x * (x - 1.0) * (x - 0.5)

        bounds = [(0, 1)]
        res = shgo(fun, bounds=bounds)
        ref = minimize_scalar(fun, bounds=bounds[0])
        assert res.success
        assert_allclose(res.fun, ref.fun)
        assert_allclose(res.x, ref.x, rtol=1e-6)

# Argument test functions
class TestShgoArguments:
    def test_1_1_simpl_iter(self):
        """Iterative simplicial sampling on TestFunction 1 (multivariate)"""
        run_test(test1_2, n=None, iters=2, sampling_method='simplicial')

    def test_1_2_simpl_iter(self):
        """Iterative simplicial on TestFunction 2 (univariate)"""
        options = {'minimize_every_iter': False}
        run_test(test2_1, n=None, iters=9, options=options,
                 sampling_method='simplicial')

    def test_2_1_sobol_iter(self):
        """Iterative Sobol sampling on TestFunction 1 (multivariate)"""
        run_test(test1_2, n=None, iters=1, sampling_method='sobol')

    def test_2_2_sobol_iter(self):
        """Iterative Sobol sampling on TestFunction 2 (univariate)"""
        res = shgo(test2_1.f, test2_1.bounds, constraints=test2_1.cons,
                   n=None, iters=1, sampling_method='sobol')

        np.testing.assert_allclose(res.x, test2_1.expected_x, rtol=1e-5, atol=1e-5)
        np.testing.assert_allclose(res.fun, test2_1.expected_fun, atol=1e-5)

    def test_3_1_disp_simplicial(self):
        """Iterative sampling on TestFunction 1 and 2  (multi and univariate)
        """

        def callback_func(x):
            print("Local minimization callback test")

        for test in [test1_1, test2_1]:
            shgo(test.f, test.bounds, iters=1,
                 sampling_method='simplicial',
                 callback=callback_func, options={'disp': True})
            shgo(test.f, test.bounds, n=1, sampling_method='simplicial',
                 callback=callback_func, options={'disp': True})

    def test_3_2_disp_sobol(self):
        """Iterative sampling on TestFunction 1 and 2 (multi and univariate)"""

        def callback_func(x):
            print("Local minimization callback test")

        for test in [test1_1, test2_1]:
            shgo(test.f, test.bounds, iters=1, sampling_method='sobol',
                 callback=callback_func, options={'disp': True})

            shgo(test.f, test.bounds, n=1, sampling_method='simplicial',
                 callback=callback_func, options={'disp': True})

    def test_args_gh14589(self):
        """Using `args` used to cause `shgo` to fail; see #14589, #15986,
        #16506"""
        res = shgo(func=lambda x, y, z: x * z + y, bounds=[(0, 3)], args=(1, 2)
                   )
        ref = shgo(func=lambda x: 2 * x + 1, bounds=[(0, 3)])
        assert_allclose(res.fun, ref.fun)
        assert_allclose(res.x, ref.x)

    def test_args_gh23517(self):
        """
        Checks that using `args` for func, jac and hess works
        """
        obj = MaratosTestArgs("a", 234)
        obj.bounds = Bounds([-5, -5], [5, 5])
        res2 = minimize(
            obj.fun,
            [4.0, 4.0],
            constraints=obj.constr,
            bounds=obj.bounds,
            method='trust-constr',
            args=("a", 234),
            jac=obj.grad
        )
        assert_allclose(res2.x, obj.x_opt, atol=1e-6)

        obj = MaratosTestArgs("a", 234)
        obj.bounds = Bounds([-10., -10.], [10., 10.])
        with warnings.catch_warnings():
            # warnings are from initialization of NonlinearConstraint and
            # poor initialization of the constraint by shgo
            warnings.simplefilter(
                "ignore",
                (OptimizeWarning, RuntimeWarning)
            )
            res = shgo(
                func=obj.fun,
                bounds=obj.bounds,
                args=("a", 234),
                minimizer_kwargs={
                    "method": 'trust-constr',
                    "constraints": obj.constr,
                    "bounds": obj.bounds,
                    "jac": obj.grad,
                    "args": ("a", 234),
                },
                constraints=obj.constr,
                sampling_method='sobol',
            )
            assert_allclose(res.x, obj.x_opt, atol=1e-6)

            res = shgo(
                func=obj.fun,
                bounds=obj.bounds,
                args=("a", 234),
                minimizer_kwargs={
                    "method": 'trust-constr',
                    "constraints": obj.constr,
                    "bounds": obj.bounds,
                    "jac": obj.grad,
                    "hess": obj.hess,
                },
                constraints=obj.constr,
                sampling_method='sobol',
            )
            assert_allclose(res.x, obj.x_opt, atol=1e-6)

    @pytest.mark.slow
    def test_4_1_known_f_min(self):
        """Test known function minima stopping criteria"""
        # Specify known function value
        options = {'f_min': test4_1.expected_fun,
                   'f_tol': 1e-6,
                   'minimize_every_iter': True}
        # TODO: Make default n higher for faster tests
        run_test(test4_1, n=None, test_atol=1e-5, options=options,
                 sampling_method='simplicial')

    @pytest.mark.slow
    def test_4_2_known_f_min(self):
        """Test Global mode limiting local evaluations"""
        options = {  # Specify known function value
            'f_min': test4_1.expected_fun,
            'f_tol': 1e-6,
            # Specify number of local iterations to perform
            'minimize_every_iter': True,
            'local_iter': 1}

        run_test(test4_1, n=None, test_atol=1e-5, options=options,
                 sampling_method='simplicial')

    def test_4_4_known_f_min(self):
        """Test Global mode limiting local evaluations for 1D funcs"""
        options = {  # Specify known function value
            'f_min': test2_1.expected_fun,
            'f_tol': 1e-6,
            # Specify number of local iterations to perform+
            'minimize_every_iter': True,
            'local_iter': 1,
            'infty_constraints': False}

        res = shgo(test2_1.f, test2_1.bounds, constraints=test2_1.cons,
                   n=None, iters=None, options=options,
                   sampling_method='sobol')
        np.testing.assert_allclose(res.x, test2_1.expected_x, rtol=1e-5, atol=1e-5)

    def test_5_1_simplicial_argless(self):
        """Test Default simplicial sampling settings on TestFunction 1"""
        res = shgo(test1_1.f, test1_1.bounds, constraints=test1_1.cons)
        np.testing.assert_allclose(res.x, test1_1.expected_x, rtol=1e-5, atol=1e-5)

    def test_5_2_sobol_argless(self):
        """Test Default sobol sampling settings on TestFunction 1"""
        res = shgo(test1_1.f, test1_1.bounds, constraints=test1_1.cons,
                   sampling_method='sobol')
        np.testing.assert_allclose(res.x, test1_1.expected_x, rtol=1e-5, atol=1e-5)

    def test_6_1_simplicial_max_iter(self):
        """Test that maximum iteration option works on TestFunction 3"""
        options = {'max_iter': 2}
        res = shgo(test3_1.f, test3_1.bounds, constraints=test3_1.cons,
                   options=options, sampling_method='simplicial')
        np.testing.assert_allclose(res.x, test3_1.expected_x, rtol=1e-5, atol=1e-5)
        np.testing.assert_allclose(res.fun, test3_1.expected_fun, atol=1e-5)

    def test_6_2_simplicial_min_iter(self):
        """Test that maximum iteration option works on TestFunction 3"""
        options = {'min_iter': 2}
        res = shgo(test3_1.f, test3_1.bounds, constraints=test3_1.cons,
                   options=options, sampling_method='simplicial')
        np.testing.assert_allclose(res.x, test3_1.expected_x, rtol=1e-5, atol=1e-5)
        np.testing.assert_allclose(res.fun, test3_1.expected_fun, atol=1e-5)

    def test_7_1_minkwargs(self):
        """Test the minimizer_kwargs arguments for solvers with constraints"""
        # Test solvers
        for solver in ['COBYLA', 'COBYQA', 'SLSQP']:
            # Note that passing global constraints to SLSQP is tested in other
            # unittests which run test4_1 normally
            minimizer_kwargs = {'method': solver,
                                'constraints': test3_1.cons}
            run_test(test3_1, n=100, test_atol=1e-3,
                     minimizer_kwargs=minimizer_kwargs,
                     sampling_method='sobol')

    def test_7_2_minkwargs(self):
        """Test the minimizer_kwargs default inits"""
        minimizer_kwargs = {'ftol': 1e-5}
        options = {'disp': True}  # For coverage purposes
        SHGO(test3_1.f, test3_1.bounds, constraints=test3_1.cons[0],
             minimizer_kwargs=minimizer_kwargs, options=options)

    def test_7_3_minkwargs(self):
        """Test minimizer_kwargs arguments for solvers without constraints"""
        for solver in ['Nelder-Mead', 'Powell', 'CG', 'BFGS', 'Newton-CG',
                       'L-BFGS-B', 'TNC', 'dogleg', 'trust-ncg', 'trust-exact',
                       'trust-krylov']:
            def jac(x):
                return np.array([2 * x[0], 2 * x[1]]).T

            def hess(x):
                return np.array([[2, 0], [0, 2]])

            minimizer_kwargs = {'method': solver,
                                'jac': jac,
                                'hess': hess}
            logging.info(f"Solver = {solver}")
            logging.info("=" * 100)
            run_test(test1_1, n=100, test_atol=1e-3,
                     minimizer_kwargs=minimizer_kwargs,
                     sampling_method='sobol')

    def test_8_homology_group_diff(self):
        options = {'minhgrd': 1,
                   'minimize_every_iter': True}

        run_test(test1_1, n=None, iters=None, options=options,
                 sampling_method='simplicial')

    def test_9_cons_g(self):
        """Test single function constraint passing"""
        SHGO(test3_1.f, test3_1.bounds, constraints=test3_1.cons[0])

    def test_10_finite_time(self):
        """Test single function constraint passing"""
        options = {'maxtime': 1e-15}

        def f(x):
            time.sleep(1e-14)
            return 0.0

        res = shgo(f, test1_1.bounds, iters=5, options=options)
        # Assert that only 1 rather than 5 requested iterations ran:
        assert res.nit == 1

    def test_11_f_min_0(self):
        """Test to cover the case where f_lowest == 0"""
        options = {'f_min': 0.0,
                   'disp': True}
        res = shgo(test1_2.f, test1_2.bounds, n=10, iters=None,
                   options=options, sampling_method='sobol')
        np.testing.assert_equal(0, res.x[0])
        np.testing.assert_equal(0, res.x[1])

    # @nottest
    @pytest.mark.skip(reason="no way of currently testing this")
    def test_12_sobol_inf_cons(self):
        """Test to cover the case where f_lowest == 0"""
        # TODO: This test doesn't cover anything new, it is unknown what the
        # original test was intended for as it was never complete. Delete or
        # replace in the future.
        options = {'maxtime': 1e-15,
                   'f_min': 0.0}
        res = shgo(test1_2.f, test1_2.bounds, n=1, iters=None,
                   options=options, sampling_method='sobol')
        np.testing.assert_equal(0.0, res.fun)

    def test_13_high_sobol(self):
        """Test init of high-dimensional sobol sequences"""

        def f(x):
            return 0

        bounds = [(None, None), ] * 41
        SHGOc = SHGO(f, bounds, sampling_method='sobol')
        # SHGOc.sobol_points(2, 50)
        SHGOc.sampling_function(2, 50)

    def test_14_local_iter(self):
        """Test limited local iterations for a pseudo-global mode"""
        options = {'local_iter': 4}
        run_test(test5_1, n=60, options=options)

    def test_15_min_every_iter(self):
        """Test minimize every iter options and cover function cache"""
        options = {'minimize_every_iter': True}
        run_test(test1_1, n=1, iters=7, options=options,
                 sampling_method='sobol')

    def test_16_disp_bounds_minimizer(self, capsys):
        """Test disp=True with minimizers that do not support bounds """
        options = {'disp': True}
        minimizer_kwargs = {'method': 'nelder-mead'}
        run_test(test1_2, sampling_method='simplicial',
                 options=options, minimizer_kwargs=minimizer_kwargs)

    def test_17_custom_sampling(self):
        """Test the functionality to add custom sampling methods to shgo"""

        def sample(n, d):
            return np.random.uniform(size=(n, d))

        run_test(test1_1, n=30, sampling_method=sample)

    def test_18_bounds_class(self):
        # test that new and old bounds yield same result
        def f(x):
            return np.square(x).sum()

        lb = [-6., 1., -5.]
        ub = [-1., 3., 5.]
        bounds_old = list(zip(lb, ub))
        bounds_new = Bounds(lb, ub)

        res_old_bounds = shgo(f, bounds_old)
        res_new_bounds = shgo(f, bounds_new)

        assert res_new_bounds.nfev == res_old_bounds.nfev
        assert res_new_bounds.message == res_old_bounds.message
        assert res_new_bounds.success == res_old_bounds.success
        x_opt = np.array([-1., 1., 0.])
        np.testing.assert_allclose(res_new_bounds.x, x_opt)
        np.testing.assert_allclose(res_new_bounds.x, res_old_bounds.x)

    @pytest.mark.fail_slow(10)
    def test_19_parallelization(self):
        """Test the functionality to add custom sampling methods to shgo"""

        with Pool(2) as p:
            run_test(test1_1, n=30, workers=p.map)  # Constrained
        run_test(test1_1, n=30, workers=map)  # Constrained
        with Pool(2) as p:
            run_test(test_s, n=30, workers=p.map)  # Unconstrained
        run_test(test_s, n=30, workers=map)  # Unconstrained

    def test_20_constrained_args(self):
        """Test that constraints can be passed to arguments"""

        def eggholder(x):
            return (
                -(x[1] + 47.0)*np.sin(np.sqrt(abs(x[0] / 2.0 + (x[1] + 47.0))))
                - x[0]*np.sin(np.sqrt(abs(x[0] - (x[1] + 47.0))))
            )

        def f(x):  # (cattle-feed)
            return 24.55 * x[0] + 26.75 * x[1] + 39 * x[2] + 40.50 * x[3]

        bounds = [(0, 1.0), ] * 4

        def g1_modified(x, i):
            return i * 2.3 * x[0] + i * 5.6 * x[1] + 11.1 * x[2] + 1.3 * x[
                3] - 5  # >=0

        def g2(x):
            return (
                12*x[0] + 11.9*x[1] + 41.8*x[2] + 52.1*x[3] - 21
                - 1.645*np.sqrt(
                    0.28*x[0]**2 + 0.19*x[1]**2 + 20.5*x[2]**2 + 0.62*x[3]**2
                )
            )  # >=0

        def h1(x):
            return x[0] + x[1] + x[2] + x[3] - 1  # == 0

        cons = ({'type': 'ineq', 'fun': g1_modified, "args": (0,)},
                {'type': 'ineq', 'fun': g2},
                {'type': 'eq', 'fun': h1})

        shgo(f, bounds, n=300, iters=1, constraints=cons)
        # using constrain with arguments AND sampling method sobol
        shgo(f, bounds, n=300, iters=1, constraints=cons,
             sampling_method='sobol')

    def test_21_1_jac_true(self):
        """Test that shgo can handle objective functions that return the
        gradient alongside the objective value. Fixes gh-13547"""
        # previous
        def func(x):
            return np.sum(np.power(x, 2)), 2 * x

        min_kwds = {"method": "SLSQP", "jac": True}
        opt_kwds = {"jac": True}
        shgo(
            func,
            bounds=[[-1, 1], [1, 2]],
            n=100, iters=5,
            sampling_method="sobol",
            minimizer_kwargs=min_kwds,
            options=opt_kwds
        )
        assert min_kwds['jac'] is True
        assert "jac" in opt_kwds

        # new
        def func(x):
            return np.sum(x ** 2), 2 * x

        bounds = [[-1, 1], [1, 2], [-1, 1], [1, 2], [0, 3]]

        res = shgo(func, bounds=bounds, sampling_method="sobol",
                   minimizer_kwargs={'method': 'SLSQP', 'jac': True})
        ref = minimize(func, x0=[1, 1, 1, 1, 1], bounds=bounds,
                       jac=True)
        assert res.success
        assert_allclose(res.fun, ref.fun)
        assert_allclose(res.x, ref.x, atol=1e-15)

        # Testing the passing of jac via options dict
        res = shgo(func, bounds=bounds, sampling_method="sobol",
                   minimizer_kwargs={'method': 'SLSQP'},
                   options={'jac': True})
        assert res.success
        assert_allclose(res.fun, ref.fun)
        assert_allclose(res.x, ref.x, atol=1e-15)

    @pytest.mark.parametrize('derivative', ['jac', 'hess', 'hessp'])
    def test_21_2_derivative_options(self, derivative):
        """shgo used to raise an error when passing `options` with 'jac'
        # see gh-12963. check that this is resolved
        """

        def objective(x):
            return 3 * x[0] * x[0] + 2 * x[0] + 5

        def gradient(x):
            return 6 * x[0] + 2

        def hess(x):
            return 6

        def hessp(x, p):
            return 6 * p

        derivative_funcs = {'jac': gradient, 'hess': hess, 'hessp': hessp}
        options = {derivative: derivative_funcs[derivative]}
        minimizer_kwargs = {'method': 'trust-constr'}

        bounds = [(-100, 100)]
        res = shgo(objective, bounds, minimizer_kwargs=minimizer_kwargs,
                   options=options)
        ref = minimize(objective, x0=[0], bounds=bounds, **minimizer_kwargs,
                       **options)

        assert res.success
        np.testing.assert_allclose(res.fun, ref.fun)
        np.testing.assert_allclose(res.x, ref.x)

    def test_21_3_hess_options_rosen(self):
        """Ensure the Hessian gets passed correctly to the local minimizer
        routine. Previous report gh-14533.
        """
        bounds = [(0, 1.6), (0, 1.6), (0, 1.4), (0, 1.4), (0, 1.4)]
        options = {'jac': rosen_der, 'hess': rosen_hess}
        minimizer_kwargs = {'method': 'Newton-CG'}
        res = shgo(rosen, bounds, minimizer_kwargs=minimizer_kwargs,
                   options=options)
        ref = minimize(rosen, np.zeros(5), method='Newton-CG',
                       **options)
        assert res.success
        assert_allclose(res.fun, ref.fun)
        assert_allclose(res.x, ref.x, atol=1e-15)

    def test_21_arg_tuple_sobol(self):
        """shgo used to raise an error when passing `args` with Sobol sampling
        # see gh-12114. check that this is resolved"""

        def fun(x, k):
            return x[0] ** k

        constraints = ({'type': 'ineq', 'fun': lambda x: x[0] - 1})

        bounds = [(0, 10)]
        res = shgo(fun, bounds, args=(1,), constraints=constraints,
                   sampling_method='sobol')
        ref = minimize(fun, np.zeros(1), bounds=bounds, args=(1,),
                       constraints=constraints)
        assert res.success
        assert_allclose(res.fun, ref.fun)
        assert_allclose(res.x, ref.x)


# Failure test functions
class TestShgoFailures:
    def test_1_maxiter(self):
        """Test failure on insufficient iterations"""
        options = {'maxiter': 2}
        res = shgo(test4_1.f, test4_1.bounds, n=2, iters=None,
                   options=options, sampling_method='sobol')

        np.testing.assert_equal(False, res.success)
        # np.testing.assert_equal(4, res.nfev)
        np.testing.assert_equal(4, res.tnev)

    def test_2_sampling(self):
        """Rejection of unknown sampling method"""
        assert_raises(ValueError, shgo, test1_1.f, test1_1.bounds,
                      sampling_method='not_Sobol')

    def test_3_1_no_min_pool_sobol(self):
        """Check that the routine stops when no minimiser is found
           after maximum specified function evaluations"""
        options = {'maxfev': 10,
                   # 'maxev': 10,
                   'disp': True}
        res = shgo(test_table.f, test_table.bounds, n=3, options=options,
                   sampling_method='sobol')
        np.testing.assert_equal(False, res.success)
        # np.testing.assert_equal(9, res.nfev)
        np.testing.assert_equal(12, res.nfev)

    def test_3_2_no_min_pool_simplicial(self):
        """Check that the routine stops when no minimiser is found
           after maximum specified sampling evaluations"""
        options = {'maxev': 10,
                   'disp': True}
        res = shgo(test_table.f, test_table.bounds, n=3, options=options,
                   sampling_method='simplicial')
        np.testing.assert_equal(False, res.success)

    def test_4_1_bound_err(self):
        """Specified bounds ub > lb"""
        bounds = [(6, 3), (3, 5)]
        assert_raises(ValueError, shgo, test1_1.f, bounds)

    def test_4_2_bound_err(self):
        """Specified bounds are of the form (lb, ub)"""
        bounds = [(3, 5, 5), (3, 5)]
        assert_raises(ValueError, shgo, test1_1.f, bounds)

    def test_5_1_1_infeasible_sobol(self):
        """Ensures the algorithm terminates on infeasible problems
           after maxev is exceeded. Use infty constraints option"""
        options = {'maxev': 100,
                   'disp': True}

        res = shgo(test_infeasible.f, test_infeasible.bounds,
                   constraints=test_infeasible.cons, n=100, options=options,
                   sampling_method='sobol')

        np.testing.assert_equal(False, res.success)

    def test_5_1_2_infeasible_sobol(self):
        """Ensures the algorithm terminates on infeasible problems
           after maxev is exceeded. Do not use infty constraints option"""
        options = {'maxev': 100,
                   'disp': True,
                   'infty_constraints': False}

        res = shgo(test_infeasible.f, test_infeasible.bounds,
                   constraints=test_infeasible.cons, n=100, options=options,
                   sampling_method='sobol')

        np.testing.assert_equal(False, res.success)

    def test_5_2_infeasible_simplicial(self):
        """Ensures the algorithm terminates on infeasible problems
           after maxev is exceeded."""
        options = {'maxev': 1000,
                   'disp': False}

        res = shgo(test_infeasible.f, test_infeasible.bounds,
                   constraints=test_infeasible.cons, n=100, options=options,
                   sampling_method='simplicial')

        np.testing.assert_equal(False, res.success)

    def test_6_1_lower_known_f_min(self):
        """Test Global mode limiting local evaluations with f* too high"""
        options = {  # Specify known function value
            'f_min': test2_1.expected_fun + 2.0,
            'f_tol': 1e-6,
            # Specify number of local iterations to perform+
            'minimize_every_iter': True,
            'local_iter': 1,
            'infty_constraints': False}
        args = (test2_1.f, test2_1.bounds)
        kwargs = {'constraints': test2_1.cons,
                  'n': None,
                  'iters': None,
                  'options': options,
                  'sampling_method': 'sobol'
                  }
        warns(UserWarning, shgo, *args, **kwargs)

    def test(self):
        from scipy.optimize import rosen, shgo
        bounds = [(0, 2), (0, 2), (0, 2), (0, 2), (0, 2)]

        def fun(x):
            fun.nfev += 1
            return rosen(x)

        fun.nfev = 0

        result = shgo(fun, bounds)
        print(result.x, result.fun, fun.nfev)  # 50


# Returns
class TestShgoReturns:
    def test_1_nfev_simplicial(self):
        bounds = [(0, 2), (0, 2), (0, 2), (0, 2), (0, 2)]

        def fun(x):
            fun.nfev += 1
            return rosen(x)

        fun.nfev = 0

        result = shgo(fun, bounds)
        np.testing.assert_equal(fun.nfev, result.nfev)

    def test_1_nfev_sobol(self):
        bounds = [(0, 2), (0, 2), (0, 2), (0, 2), (0, 2)]

        def fun(x):
            fun.nfev += 1
            return rosen(x)

        fun.nfev = 0

        result = shgo(fun, bounds, sampling_method='sobol')
        np.testing.assert_equal(fun.nfev, result.nfev)


def test_vector_constraint():
    # gh15514
    def quad(x):
        x = np.asarray(x)
        return [np.sum(x ** 2)]

    nlc = NonlinearConstraint(quad, [2.2], [3])
    oldc = new_constraint_to_old(nlc, np.array([1.0, 1.0]))

    res = shgo(rosen, [(0, 10), (0, 10)], constraints=oldc, sampling_method='sobol')
    assert np.all(np.sum((res.x)**2) >= 2.2)
    assert np.all(np.sum((res.x) ** 2) <= 3.0)
    assert res.success


@pytest.mark.filterwarnings("ignore:delta_grad")
def test_trust_constr():
    def quad(x):
        x = np.asarray(x)
        return [np.sum(x ** 2)]

    nlc = NonlinearConstraint(quad, [2.6], [3])
    minimizer_kwargs = {'method': 'trust-constr'}
    # note that we don't supply the constraints in minimizer_kwargs,
    # so if the final result obeys the constraints we know that shgo
    # passed them on to 'trust-constr'
    res = shgo(
        rosen,
        [(0, 10), (0, 10)],
        constraints=nlc,
        sampling_method='sobol',
        minimizer_kwargs=minimizer_kwargs
    )
    assert np.all(np.sum((res.x)**2) >= 2.6)
    assert np.all(np.sum((res.x) ** 2) <= 3.0)
    assert res.success


def test_equality_constraints():
    # gh16260
    bounds = [(0.9, 4.0)] * 2  # Constrain probabilities to 0 and 1.

    def faulty(x):
        return x[0] + x[1]

    nlc = NonlinearConstraint(faulty, 3.9, 3.9)
    res = shgo(rosen, bounds=bounds, constraints=nlc)
    assert_allclose(np.sum(res.x), 3.9)

    def faulty(x):
        return x[0] + x[1] - 3.9

    constraints = {'type': 'eq', 'fun': faulty}
    res = shgo(rosen, bounds=bounds, constraints=constraints)
    assert_allclose(np.sum(res.x), 3.9)

    bounds = [(0, 1.0)] * 4
    # sum of variable should equal 1.
    def faulty(x):
        return x[0] + x[1] + x[2] + x[3] - 1

    # options = {'minimize_every_iter': True, 'local_iter':10}
    constraints = {'type': 'eq', 'fun': faulty}
    res = shgo(
        lambda x: - np.prod(x),
        bounds=bounds,
        constraints=constraints,
        sampling_method='sobol'
    )
    assert_allclose(np.sum(res.x), 1.0)

def test_gh16971():
    def cons(x):
        return np.sum(x**2) - 0

    c = {'fun': cons, 'type': 'ineq'}
    minimizer_kwargs = {
        'method': 'COBYLA',
        'options': {'rhobeg': 5, 'tol': 5e-1, 'catol': 0.05}
    }

    s = SHGO(
        rosen, [(0, 10)]*2, constraints=c, minimizer_kwargs=minimizer_kwargs
    )

    assert s.minimizer_kwargs['method'].lower() == 'cobyla'
    assert s.minimizer_kwargs['options']['catol'] == 0.05
