# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and Engineering
# Solutions of Sandia, LLC, the U.S. Government retains certain rights in this
# software.  This software is distributed under the 3-clause BSD License.
# ____________________________________________________________________________________

import logging
from itertools import product

from io import StringIO

import pyomo.common.unittest as unittest
from pyomo.common.log import LoggingIntercept
from pyomo.contrib.multistart.high_conf_stop import should_stop
from pyomo.contrib.multistart.reinit import strategies
from pyomo.environ import (
    ConcreteModel,
    Constraint,
    NonNegativeReals,
    Objective,
    SolverFactory,
    Var,
    maximize,
    sin,
    value,
)


@unittest.skipIf(not SolverFactory('ipopt').available(), "IPOPT not available")
class MultistartTests(unittest.TestCase):
    """
    Due to stochastic nature of the random restarts, these tests just
    demonstrate, that for a small sample, the test will not do worse than the
    standard solver. this is non-exhaustive due to the randomness. Hence all
    asserts are inequalities.
    """

    def test_as_good_as_standard(self):
        standard_model = build_model()
        SolverFactory('ipopt').solve(standard_model)
        standard_objective_value = value(
            next(standard_model.component_data_objects(Objective, active=True))
        )

        fresh_model = build_model()
        multistart_iterations = 10
        test_trials = 10
        for strategy, _ in product(strategies.keys(), range(test_trials)):
            m2 = fresh_model.clone()
            SolverFactory('multistart').solve(
                m2, iterations=multistart_iterations, strategy=strategy
            )
            clone_objective_value = value(
                next(m2.component_data_objects(Objective, active=True))
            )
            self.assertGreaterEqual(
                clone_objective_value, standard_objective_value
            )  # assumes maximization

    def test_as_good_with_HCS_rule(self):
        """test that the high confidence stopping rule with very lenient
        parameters does no worse.
        """
        # initialize model with data
        m = build_model()

        # create ipopt solver
        SolverFactory('ipopt').solve(m)
        for i in range(5):
            m2 = build_model()
            SolverFactory('multistart').solve(
                m2, iterations=-1, stopping_mass=0.99, stopping_delta=0.99
            )
            m_objectives = m.component_data_objects(Objective, active=True)
            m_obj = next(m_objectives, None)
            m2_objectives = m2.component_data_objects(Objective, active=True)
            m2_obj = next(m2_objectives, None)
            # Assert that multistart solver does no worse than standard solver
            self.assertTrue((value(m2_obj.expr)) >= (value(m_obj.expr) - 0.001))
            del m2

    def test_missing_bounds(self):
        m = ConcreteModel()
        m.x = Var(domain=NonNegativeReals)
        m.obj = Objective(expr=m.x)
        output = StringIO()
        with LoggingIntercept(output, 'pyomo.contrib.multistart', logging.WARNING):
            SolverFactory('multistart').solve(m)
            self.assertIn(
                "Skipping reinitialization of unbounded "
                "variable x with bounds (0, None).",
                output.getvalue().strip(),
            )

    def test_var_value_None(self):
        m = ConcreteModel()
        m.x = Var(bounds=(0, 1))
        m.obj = Objective(expr=m.x)
        SolverFactory('multistart').solve(m)

    def test_model_infeasible(self):
        m = ConcreteModel()
        m.x = Var(bounds=(0, 1))
        m.c = Constraint(expr=m.x >= 2)
        m.o = Objective(expr=m.x)
        SolverFactory('multistart').solve(m, iterations=2)
        output = StringIO()
        with LoggingIntercept(output, 'pyomo.contrib.multistart', logging.WARNING):
            SolverFactory('multistart').solve(m, iterations=-1, HCS_max_iterations=3)
            self.assertIn(
                "High confidence stopping rule was unable to "
                "complete after 3 iterations.",
                output.getvalue().strip(),
            )

    def test_should_stop(self):
        soln = [0] * 149
        self.assertFalse(should_stop(soln, 0.5, 0.5, 0.001))
        soln += [0.001]
        self.assertTrue(should_stop(soln, 0.5, 0.5, 0.001))
        soln = [0] * 149 + [0.01]
        self.assertFalse(should_stop(soln, 0.5, 0.5, 0.001))
        soln = [0] * 149 + [-0.001]
        self.assertTrue(should_stop(soln, 0.5, 0.5, 0.001))

    def test_multiple_obj(self):
        m = ConcreteModel()
        m.x = Var()
        m.o = Objective(expr=m.x)
        m.o2 = Objective(expr=m.x)
        with self.assertRaisesRegex(RuntimeError, "multiple active objectives"):
            SolverFactory('multistart').solve(m)

    def test_no_obj(self):
        m = ConcreteModel()
        m.x = Var()
        with self.assertRaisesRegex(RuntimeError, "no active objective"):
            SolverFactory('multistart').solve(m)

    def test_const_obj(self):
        m = ConcreteModel()
        m.x = Var()
        m.o = Objective(expr=5)
        with self.assertRaisesRegex(RuntimeError, "constant objective"):
            SolverFactory('multistart').solve(m)


def build_model():
    """Simple non-convex model with many local minima"""
    model = ConcreteModel()
    model.x1 = Var(initialize=1, bounds=(0, 100))
    model.x2 = Var(initialize=5, bounds=(5, 6))
    model.x2.fix(5)
    model.objtv = Objective(expr=model.x1 * sin(model.x1), sense=maximize)
    return model


if __name__ == '__main__':
    unittest.main()
