#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2022
#  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.
#  ___________________________________________________________________________

#
# Unit Tests for ContinuousSet() Objects
#

import os
from os.path import abspath, dirname

import pyomo.common.unittest as unittest

from pyomo.environ import ConcreteModel, AbstractModel, Set
from pyomo.dae import ContinuousSet
from pyomo.common.log import LoggingIntercept
from io import StringIO

currdir = dirname(abspath(__file__)) + os.sep


class TestContinuousSet(unittest.TestCase):

    # test __init__
    def test_init(self):
        model = ConcreteModel()
        model.t = ContinuousSet(bounds=(0, 1))

        model = ConcreteModel()
        model.t = ContinuousSet(initialize=[1, 2, 3])

        model = ConcreteModel()
        model.t = ContinuousSet(bounds=(0, 5), initialize=[1, 3, 5])

        # Expected ValueError because a ContinuousSet component
        # must contain at least two values upon construction
        with self.assertRaises(ValueError):
            model.t = ContinuousSet()

    # test bad keyword arguments
    def test_bad_kwds(self):
        model = ConcreteModel()
        with self.assertRaises(TypeError):
            model.t = ContinuousSet(bounds=(0, 1), filter=True)

        with self.assertRaises(TypeError):
            model.t = ContinuousSet(bounds=(0, 1), dimen=2)

        with self.assertRaises(TypeError):
            model.t = ContinuousSet(bounds=(0, 1), virtual=True)

        with self.assertRaises(TypeError):
            model.t = ContinuousSet(bounds=(0, 1), validate=True)

    # test valid declarations
    def test_valid_declaration(self):
        model = ConcreteModel()
        model.t = ContinuousSet(bounds=(0, 1))
        self.assertEqual(len(model.t), 2)
        self.assertIn(0, model.t)
        self.assertIn(1, model.t)

        model = ConcreteModel()
        model.t = ContinuousSet(initialize=[1, 2, 3])
        self.assertEqual(len(model.t), 3)
        self.assertEqual(model.t.first(), 1)
        self.assertEqual(model.t.last(), 3)

        model = ConcreteModel()
        model.t = ContinuousSet(bounds=(1, 3), initialize=[1, 2, 3])
        self.assertEqual(len(model.t), 3)
        self.assertEqual(model.t.first(), 1)
        self.assertEqual(model.t.last(), 3)

        model = ConcreteModel()
        model.t = ContinuousSet(bounds=(0, 4), initialize=[1, 2, 3])
        self.assertEqual(len(model.t), 5)
        self.assertEqual(model.t.first(), 0)
        self.assertEqual(model.t.last(), 4)

        model = ConcreteModel()
        with self.assertRaisesRegex(
                ValueError, r"value is not in the domain \[0..4\]"):
            model.t = ContinuousSet(bounds=(0, 4), initialize=[1, 2, 3, 5])
        # self.assertEqual(len(model.t), 5)
        # self.assertEqual(model.t.first(), 0)
        # self.assertEqual(model.t.last(), 5)
        # self.assertNotIn(4, model.t)
        # del model.t

        model = ConcreteModel()
        with self.assertRaisesRegex(
                ValueError, r"value is not in the domain \[2..6\]"):
            model.t = ContinuousSet(bounds=(2, 6), initialize=[1, 2, 3, 5])
        # self.assertEqual(len(model.t), 5)
        # self.assertEqual(model.t.first(), 1)
        # self.assertEqual(model.t.last(), 6)
        # del model.t

        model = ConcreteModel()
        with self.assertRaisesRegex(
                ValueError, r"value is not in the domain \[2..4\]"):
            model.t = ContinuousSet(bounds=(2, 4), initialize=[1, 3, 5])
        # self.assertEqual(len(model.t), 3)
        # self.assertNotIn(2, model.t)
        # self.assertNotIn(4, model.t)

    # test invalid declarations
    def test_invalid_declaration(self):
        model = ConcreteModel()
        model.s = Set(initialize=[1, 2, 3])
        with self.assertRaises(TypeError):
            model.t = ContinuousSet(model.s, bounds=(0, 1))

        model = ConcreteModel()
        with self.assertRaises(ValueError):
            model.t = ContinuousSet(bounds=(0, 0))

        model = ConcreteModel()
        with self.assertRaises(ValueError):
            model.t = ContinuousSet(initialize=[1])

        model = ConcreteModel()
        with self.assertRaises(ValueError):
            model.t = ContinuousSet(bounds=(None, 1))

        model = ConcreteModel()
        with self.assertRaises(ValueError):
            model.t = ContinuousSet(bounds=(0, None))

        model = ConcreteModel()
        with self.assertRaises(ValueError):
            model.t = ContinuousSet(initialize=[(1, 2), (3, 4)])

        model = ConcreteModel()
        with self.assertRaises(ValueError):
            model.t = ContinuousSet(initialize=['foo', 'bar'])

    # test the get_changed method
    def test_get_changed(self):
        model = ConcreteModel()
        model.t = ContinuousSet(initialize=[1, 2, 3])
        self.assertFalse(model.t.get_changed())
        self.assertEqual(model.t._changed, model.t.get_changed())

    # test the set_changed method
    def test_set_changed(self):
        model = ConcreteModel()
        model.t = ContinuousSet(initialize=[1, 2, 3])
        self.assertFalse(model.t._changed)
        model.t.set_changed(True)
        self.assertTrue(model.t._changed)
        model.t.set_changed(False)
        self.assertFalse(model.t._changed)

        with self.assertRaises(ValueError):
            model.t.set_changed(3)

    # test get_upper_element_boundary
    def test_get_upper_element_boundary(self):
        m = ConcreteModel()
        m.t = ContinuousSet(initialize=[1, 2, 3])
        self.assertEqual(m.t.get_upper_element_boundary(1.5), 2)
        self.assertEqual(m.t.get_upper_element_boundary(2.5), 3)
        self.assertEqual(m.t.get_upper_element_boundary(2), 2)

        log_out = StringIO()
        with LoggingIntercept(log_out, 'pyomo.dae'):
            temp = m.t.get_upper_element_boundary(3.5)
        self.assertIn('Returning the upper bound', log_out.getvalue())

    # test get_lower_element_boundary
    def test_get_lower_element_boundary(self):
        m = ConcreteModel()
        m.t = ContinuousSet(initialize=[1, 2, 3])
        self.assertEqual(m.t.get_lower_element_boundary(1.5), 1)
        self.assertEqual(m.t.get_lower_element_boundary(2.5), 2)
        self.assertEqual(m.t.get_lower_element_boundary(2), 2)

        log_out = StringIO()
        with LoggingIntercept(log_out, 'pyomo.dae'):
            temp = m.t.get_lower_element_boundary(0.5)
        self.assertIn('Returning the lower bound', log_out.getvalue())

    def test_duplicate_construct(self):
        m = ConcreteModel()
        m.t = ContinuousSet(initialize=[1,2,3])
        self.assertEqual(m.t, [1,2,3])
        self.assertEqual(m.t._fe, [1,2,3])
        m.t.add(1.5)
        m.t.add(2.5)
        self.assertEqual(m.t, [1,1.5,2,2.5,3])
        self.assertEqual(m.t._fe, [1,2,3])
        m.t.construct()
        self.assertEqual(m.t, [1,1.5,2,2.5,3])
        self.assertEqual(m.t._fe, [1,2,3])

    def test_find_nearest_index(self):
        m = ConcreteModel()
        m.time = ContinuousSet(bounds=(0, 5))

        # Undiscretized
        i = m.time.find_nearest_index(1)
        self.assertEqual(i, 1)
        i = m.time.find_nearest_index(1, tolerance=0.5)
        self.assertEqual(i, None)

        i = m.time.find_nearest_index(-0.01, tolerance=0.1)
        self.assertEqual(i, 1)
        i = m.time.find_nearest_index(-0.01, tolerance=0.001)
        self.assertEqual(i, None)

        i = m.time.find_nearest_index(6, tolerance=2)
        self.assertEqual(i, 2)
        i = m.time.find_nearest_index(6, tolerance=1)
        self.assertEqual(i, 2)

        # This test relies on the behavior for tiebreaks
        i = m.time.find_nearest_index(2.5)
        self.assertEqual(i, 1)

        m.del_component(m.time)
        init_list = []
        for i in range(5):
            i0 = float(i)
            i1 = round((i+0.15)*1e4)/1e4
            i2 = round((i+0.64)*1e4)/1e4
            # Round to get rid of numerical error due to float addition
            init_list.extend([i, i1, i2])
        init_list.append(5.0)
        m.time = ContinuousSet(initialize=init_list)

        # "Discretized"
        i = m.time.find_nearest_index(1.01, tolerance=0.1)
        self.assertEqual(i, 4)
        i = m.time.find_nearest_index(1.01, tolerance=0.001)
        self.assertEqual(i, None)

        i = m.time.find_nearest_index(3.5)
        self.assertEqual(i, 12)
        i = m.time.find_nearest_index(3.5, tolerance=0.1)
        self.assertEqual(i, None)

        i = m.time.find_nearest_index(-1)
        self.assertEqual(i, 1)
        i = m.time.find_nearest_index(-1, tolerance=1)
        self.assertEqual(i, 1)

        i = m.time.find_nearest_index(5.5)
        self.assertEqual(i, 16)
        i = m.time.find_nearest_index(5.5, tolerance=0.49)
        self.assertEqual(i, None)

        i = m.time.find_nearest_index(2.64, tolerance=1e-8)
        self.assertEqual(i, 9)
        i = m.time.find_nearest_index(2.64, tolerance=0)
        self.assertEqual(i, 9)

        i = m.time.find_nearest_index(5, tolerance=0)
        self.assertEqual(i, 16)

        i = m.time.find_nearest_index(0, tolerance=0)
        self.assertEqual(i, 1)

        # This test fails. I get:
        # delta_left == 2.075-2.0 == 0.07500000000000018
        # delta_right == 2.15-2.075 == 0.07499999999999973
        # Index 8 is returned (time[8] == 2.15), even though
        # "tie"-break logic suggests index 7 should be.
        #i = m.time.find_nearest_index(2.075)
        #self.assertEqual(i, 7)


class TestIO(unittest.TestCase):

    def setUp(self):
        #
        # Create Model
        #
        self.model = AbstractModel()
        self.instance = None

    def tearDown(self):
        if os.path.exists("diffset.dat"):
            os.remove("diffset.dat")
        self.model = None
        self.instance = None

    def test_io1(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set A := 1 3 5 7;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()
        self.model.A = ContinuousSet()
        self.instance = self.model.create_instance("diffset.dat")
        self.assertEqual(len(self.instance.A), 4)

    def test_io2(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set A := 1 3 5;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()
        self.model.A = ContinuousSet(bounds=(0, 4))
        with self.assertRaisesRegex(
                ValueError, r"The value is not in the domain \[0..4\]"):
            self.instance = self.model.create_instance("diffset.dat")
        #self.assertEqual(len(self.instance.A), 4)

    def test_io3(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set A := 1 3 5;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()
        self.model.A = ContinuousSet(bounds=(2, 6))
        with self.assertRaisesRegex(
                ValueError, r"The value is not in the domain \[2..6\]"):
            self.instance = self.model.create_instance("diffset.dat")
        #self.assertEqual(len(self.instance.A), 4)

    def test_io4(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set A := 1 3 5;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()
        self.model.A = ContinuousSet(bounds=(2, 4))
        with self.assertRaisesRegex(
                ValueError, r"The value is not in the domain \[2..4\]"):
            self.instance = self.model.create_instance("diffset.dat")
        #self.assertEqual(len(self.instance.A), 3)

    def test_io5(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set A := 1 3 5;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()
        self.model.A = ContinuousSet(bounds=(0, 6))
        self.instance = self.model.create_instance("diffset.dat")
        self.assertEqual(len(self.instance.A), 5)

    def test_io6(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set B := 1;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()

        # Expected ValueError because data set has only one value and no
        # bounds are specified
        self.model.B = ContinuousSet()
        with self.assertRaises(ValueError):
            self.instance = self.model.create_instance("diffset.dat")

    def test_io7(self):
        OUTPUT = open("diffset.dat", "w")
        OUTPUT.write("data;\n")
        OUTPUT.write("set B := 1;\n")
        OUTPUT.write("end;\n")
        OUTPUT.close()
        self.model.B = ContinuousSet(bounds=(0, 1))
        self.instance = self.model.create_instance("diffset.dat")
        self.assertEqual(len(self.instance.B), 2)

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