r"""
Interactive Simplex Method floating-point helper class

This module provides a "clean" dictionary view on a dictionary with
floating-point numbers.  Cleaning means to change almost-zeros to exact
zeros, allowing the Interactive Simplex Method to recognize primal and
dual feasibility and to avoid pivoting on zero pivot elements.  By
extension, it also changes almost-integers to exact integers, for the
benefit of mixed-integer programming.
"""

from sage.numerical.interactive_simplex_method \
    import LPDictionary, LPAbstractDictionary
from sage.modules.all import vector
from sage.rings.all import ZZ
from sage.matrix.all import matrix
from sage.misc.functional import round

class LPCleanDictionary(LPAbstractDictionary):
    r"""
    Construct a clean dictionary for an LP problem from a dictionary.

    INPUT:

        - ``dictionary`` -- the dictionary to be cleaned

        - ``epsilon`` -- (default: 1e-9) the tolerance of the cleaning process

    OUTPUT:

       - a :class:`dictionary for an LP problem
       <LPCleanDictionary>`

    EXAMPLES:

    One needs an instance inherited from :class:`LPAbstractDictionary`
    to initialize this class:

    Setting up a backend dictionary as the input::

        sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
        sage: x = p.new_variable(nonnegative=True)
        sage: p.add_constraint(-x[0] + x[1] <= 2)
        sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
        sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
        sage: lp, basis = p.interactive_lp_problem()
        sage: d = lp.dictionary(*basis)
        sage: view(d)  # not tested

    Create the clean dictionary using the backend dictionary::

        sage: from sage_numerical_interactive_mip.clean_dictionary \
              import LPCleanDictionary
        sage: clean = LPCleanDictionary(d, epsilon=1e-5)
        sage: TestSuite(clean).run(skip=['_test_not_implemented_methods', '_test_pickling'])
        sage: clean
        LP problem dictionary (use typeset mode to see details)
        sage: view(clean.dictionary())  # not tested
    """
    def __init__(self, dictionary, epsilon=1e-9):
        r"""
        See :class:`LPGLPKBackendDictionary` for documentation.

        TESTS::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: view(d)  # not tested
            sage: clean = LPCleanDictionary(d, epsilon=1e-5)
            sage: clean
            LP problem dictionary (use typeset mode to see details)
            sage: view(clean.dictionary())  # not tested
        """
        super(LPCleanDictionary, self).__init__()
        self._dict = dictionary
        self._epsilon = epsilon

    def _round_(self, var):
        r"""
        Round a number based on the epsilon.

        INPUT:

            - ``var`` -- the variable to be rounded

        EXAMPLE::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean._round_(0.1)
            0.100000000000000
            sage: clean._round_(0.00001)
            0
        """
        nearest_int = ZZ(round(var))
        if abs(var - nearest_int) <= self._epsilon:
            var = nearest_int
        return var

    def leave(self, v):
        r"""
        Set ``v`` as the leaving variable for the associated dictionary.

        INPUT:

        - ``v`` -- a basic variable of the associated dictionary,
        can be given as a string, an actual variable, or an integer
        interpreted as the index of a variable

        EXAMPLE::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.basic_variables()
            (w_0, w_1)
            sage: clean.leave(clean.basic_variables()[0])
            sage: d.leaving()
            w_0
        """
        super(LPCleanDictionary, self).leave(v)
        self._dict.leave(v)

    def enter(self, v):
        r"""
        Set ``v`` as the entering variable for the associated dictionary.

        INPUT:

        - ``v`` -- a basic variable of the associated dictionary,
        can be given as a string, an actual variable, or an integer
        interpreted as the index of a variable

        EXAMPLE::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.nonbasic_variables()
            (m_0, m_1)
            sage: clean.enter(clean.nonbasic_variables()[0])
            sage: d._entering
            m_0
        """
        super(LPCleanDictionary, self).enter(v)
        self._dict.enter(v)

    def _round_all_(self, iterable):
        r"""
        Round all element in an iterable.

        INPUT:

            - ``iterable`` -- the list to tranverse through

        EXAMPLE::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: l = [0.01, 0.000001]
            sage: clean._round_all_(l)
            (0.0100000000000000, 0.000000000000000)
        """
        return vector([self._round_(e) for e in iterable])

    def row_coefficients(self, v):
        r"""
        Return the coefficients of a basic variable

        INPUT:

        - ``v`` -- a basic variable of ``self``, can be given as a string, an
          actual variable, or an integer interpreted as the index of a variable

        OUTPUT:

        - a vector of coefficients of a basic variable

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.row_coefficients(clean.basic_variables()[0])
            (-1, 1)
        """
        coefs = self._dict.row_coefficients(v)
        return self._round_all_(coefs)

    def constant_terms(self):
        r"""
        Return the constant terms of relations of ``self``.

        OUTPUT:

        - a vector.

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: d.constant_terms()
            (2.0, 3e-07)
            sage: clean.constant_terms()
            (2, 0)
        """
        terms = self._dict.constant_terms()
        return self._round_all_(terms)

    def objective_coefficients(self):
        r"""
        Return coefficients of the objective of ``self``.

        OUTPUT:

        - a vector

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.objective_coefficients()
            (-5.5, -2.1)
        """
        coefs = self._dict.objective_coefficients()
        return self._round_all_(coefs)

    def objective_name(self):
        return self._dict.objective_name()

    def objective_value(self):
        r"""
        Return the value of the objective value.

        OUTPUT:

        - a number

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.objective_value()
            0
        """
        val = self._dict.objective_value()
        return self._round_(val)

    def basic_variables(self):
        r"""
        Return the basic variables of the associated dictionary.

        OUTPUT:

        - a vector

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.basic_variables()
            (w_0, w_1)
        """
        return self._dict.basic_variables()

    def nonbasic_variables(self):
        r"""
        Return the non-basic variables of the associated dictionary.

        OUTPUT:

        - a vector

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean.nonbasic_variables()
            (m_0, m_1)
        """
        return self._dict.nonbasic_variables()

    def dictionary(self):
        r"""
        Return a cleaned regular LP dictionary of the associated dictionary.

        OUTPUT:

        - an :class:`LP dictionary <LPDictionary>`

        EXAMPLES::

            sage: from sage_numerical_interactive_mip.clean_dictionary \
                  import LPCleanDictionary
            sage: p = MixedIntegerLinearProgram(names=['m'], solver="GLPK")
            sage: x = p.new_variable(nonnegative=True)
            sage: p.add_constraint(-x[0] + x[1] <= 2)
            sage: p.add_constraint(0.001 * x[0] + 0.0005 * x[1] <= 0.0000003)
            sage: p.set_objective(-5.5 * x[0] + -2.1 * x[1])
            sage: lp, basis = p.interactive_lp_problem()
            sage: d = lp.dictionary(*basis)
            sage: clean = LPCleanDictionary(d, epsilon=1e-4)
            sage: clean_dict = clean.dictionary()
            sage: view(clean_dict)  #not tested
        """
        old_leaving = self._dict._leaving
        rows = []
        for i, var in enumerate(self.basic_variables()):
            self.leave(self.basic_variables()[i])
            rows.append(self.leaving_coefficients())

        A = matrix(rows)
        D = LPDictionary(A,
                         self.constant_terms(),
                         self.objective_coefficients(),
                         self.objective_value(),
                         self.basic_variables(),
                         self.nonbasic_variables(),
                         self.objective_name())
        self.leave(old_leaving)
        D.enter(self._dict._entering)
        D.leave(old_leaving)
        return D
