..  Copyright (c) 2013-2020, SIB - Swiss Institute of Bioinformatics and
..                           Biozentrum - University of Basel
..  
..  Licensed under the Apache License, Version 2.0 (the "License");
..  you may not use this file except in compliance with the License.
..  You may obtain a copy of the License at
..  
..    http://www.apache.org/licenses/LICENSE-2.0
..  
..  Unless required by applicable law or agreed to in writing, software
..  distributed under the License is distributed on an "AS IS" BASIS,
..  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
..  See the License for the specific language governing permissions and
..  limitations under the License.


Graph Minimizer
================================================================================

.. currentmodule:: promod3.core

The graph minimizer solves an energy minimization problem where we have n
nodes :math:`N_i`, with each node having several possible solutions. 
Every solution has a self energy :math:`E_{self}` and pairwise energies in between nodes
are possible. The goal is to select exactly one solution per node to obtain 
a set :math:`X=[x_1, x_2, ..., x_n]` that minimizes:

.. math::
  F(X)=\displaystyle\sum_iE_{self}(N_i[x_i]) +\displaystyle \sum_i \displaystyle \sum_{j>i}E_{pair}(N_i[x_i], N_j[x_j])



.. class:: GraphMinimizer

  .. method:: AddNode(self_energies) 

    Adds a node to the graph. 

    :param self_energies: Directly controls the number of possible solutions in that
                          node and assigns the corresponding self energies
    :type self_energies:  :class:`list` of :class:`float`

    :returns:           The idx of the added node
    :rtype:             :class:`int`


  .. method:: AddEdge(node_idx_one, node_idx_two, pairwise_energies) 

    Adds an edge between the specified nodes.

    :param node_idx_one: Index of the first node
    :param node_idx_two: Index of the second node
    :param pairwise_energies: The pairwise energies that contribute to the 
                              overall energy function. There must be a list for
                              every possible solution in node one. All of those
                              lists must have exactly the length of possible 
                              solutions in node two.

    :type node_idx_one: :class:`int`
    :type node_idx_two: :class:`int`
    :type pairwise_energies: :class:`list` of :class:`list` of :class:`float`

    :returns:           The idx of the added edge
    :rtype:             :class:`int`
    :raises: :exc:`~exceptions.RuntimeError` if *node_idx_one* or *node_idx_two* 
             specify non existent nodes or when *pairwise_energies* is 
             inconsistent with the number of solutions in the specified nodes.


  .. method:: ApplyDEE(node_idx, [e_cut=0.0])

    Applies dead end elimination on one particular node and potentially
    deactivates certain solutions. The goldstein criterion is described in 
    [goldstein1994]_.

    :param node_idx:    Node to apply dead end elimination
    :param e_cut:       If set to
                        0.0, the default goldstein criterion is applied =>
                        a solution is removed if it's dominated by all other
                        solutions in the same node. If you increase this value,
                        a solution must be dominated by at least this **e_cut**.

    :type node_idx:     :class:`int`
    :type e_cut:        :class:`float`

    :returns:           :class:`bool` whether any solution has been deactivated.


  .. method:: ApplyEdgeDecomposition(edge_idx, epsilon)

    Applies edge decomposition on one particular edge and potentially
    deactivates it. The exact decomposition  procedure is described in 
    [krivov2009]_.

    :param edge_idx:    Edge to decompose. 
    :param epsilon:     The energy threshold to perform edge decomposition.

    :type edge_idx:     :class:`int`
    :type epsilon:      :class:`float`


    :returns:           :class:`bool` whether the edge has been decomposed and 
                        deactivated 



  .. method:: Prune(epsilon, [e_cut=0.0, consider_all_nodes=False])

    Performs edge decomposition followed by dead end elimination in an
    iterative manner until no changes can be observed anymore given 
    **epsilon**.

    :param epsilon:     The energy threshold to perform edge decomposition.
    :param e_cut:       Parameter to control dead end elimination.
    :param consider_all_nodes: Flag, wether the dead end elimination should be
                               applied to all nodes, or only those who are
                               connected with an edge removed by edge 
                               decomposition. This is useful if already a Prune
                               operation has been performed => only those nodes
                               connected to a removed edge have the chance for
                               successful dead end elimination.

    :type epsilon:      :class:`float`
    :type e_cut:        :class:`float`
    :type consider_all_nodes: :class:`bool`
                        

  .. method:: Reset()

    Resets the graph by undoing any pruning operation (DEE and edge decomposition)


  .. method:: TreeSolve([max_complexity=inf, initial_epsilon=0.02])

    The method solves a graph using a minimal width tree decomposition
    approach in an iterative manner. In every iteration, the algorithm performs
    a pruning step (DEE / Edge Decomposition) and subsequently tries to solve 
    each connected component in the graph separately. 
    If the number of possible enumerations in the tree constructetd from a 
    particular connected component is is larger **max_complexity**, 
    this component is solved in a later iteration. The algorithm iterates until 
    all connected components are solved and steadily increases the epsilon value 
    resulting in a more and more agressive edge decomposition.
    Algorithm further descsribed in [krivov2009]_.

    :param max_complexity: Max number of possible permutations, that have to 
                           be enumerated in the resulting tree after tree
                           decomposition.

    :param initial_epsilon: The initial energy threshold to perform edge
                            decomposition.

    :type max_complexity: :class:`int`
    :type initial_epsilon: :class:`float`

    :returns:           A tuple with the first element being a list of indices 
                        representing the single solutions minimizing
                        the overall energy function.
                        The second element is the according energy value.


  .. method:: AStarSolve(e_thresh, [max_n=100, max_visited_nodes=100000000])

    The method solves a graph using the A\* algorithm. Besides creating the
    minimal energy solution, the function produces a maximum of **max_n**
    solutions sorted by energy. It aborts as soon as it sees the first solution
    with an energy difference of **e_thresh** to the optimal solution or hits
    **max_n**. If you're only interested in the optimal solution you should use
    the TreeSolve function since it's much faster and uses less memory.
    There is no automatic pruning of the graph using DEE or edge decomposition, 
    so you have to do it by yourself, otherwise you'll have a looooooong 
    runtime or even hit the **max_visited_nodes** parameter that caps the memory
    usage. 
    To have a valid solution you have to take care that you set the **e_cut** 
    parameter in the pruning function to **e_tresh**.
    Algorithm is described in [leach1998]_.

    :param e_thresh:    Maximal energy difference of a solution to the
                        optimal solution to be considered.

    :param max_n:       The maximum number of solutions that will be generated.

    :param  max_visited_nodes: Caps the memory usage of the algorithm. Besides
                               The memory used for pairwise energies and self
                               energies, the algorithm uses about 
                               **max_visited_nodes** * 20 bytes.


    :type e_thresh:      :class:`float`
    :type max_n:        :class:`int`
    :type max_visited_nodes: :class:`int`

    :returns:           A tuple with the first element being a list of 
                        solutions. The second element is a list
                        of energies for the according solutions.


  .. method:: MCSolve([n=100, mc_steps=100000, start_temperature=1000.0, \\
                       change_frequency=1000, cooling_factor=0.9, seed=0]) 

    Does a total of **n** Monte Carlo runs to find low energy solutions
    of the graph. Each run starts with a random 
    configuration. At each of the **mc_steps** steps, a random node and
    a random solution at that location is selected and an energy difference 
    of that random selection relative to the current configuration is 
    estimated. If the difference in energy is negative, the step is 
    accepted. If not, the step is accepted with a probability given by
    the temperature dependent Metropolis criterion
    :math:`exp^{\left(\frac{-e_{diff}}{T}\right)}`.
    The temperature for every run starts with **start_temperature** 
    and is multiplied every **change_frequency** steps with **cooling_factor**
    to achieve a simulated annealing effect.
    There is no automatic pruning of the graph using DEE or edge decomposition,
    you have to do that by yourself.
    
    :param n:           Number of Monte Carlo runs 
    :param mc_steps:    Number of Monte Carlo steps per run
    :param start_temperature: Start temperature for the temperature dependent
                              Metropolis criterion
    :param change_frequency: Number of steps the temperature stays the same
    :param cooling_factor: Factor to multiply temperature each 
                          **change_frequency** steps
    :param seed:        Seed for random number generator 

    :type n:            :class:`int`
    :type mc_steps:     :class:`int`
    :type start_temperature: :class:`float`
    :type change_frequency: :class:`int`
    :type cooling_factor: :class:`float`
    :type seed:         :class:`float`

    :returns:           A tuple with the first element being a list of 
                        solutions. The second element is a list
                        of energies for the according solutions.

  .. method:: NaiveSolve()

    Don't even think of using this... This function only exists for debug 
    purposes and does the full enumeration of the solution space. 
    It might become faster with the appropriate  
    `techniques <https://www.youtube.com/watch?v=fQGbXmkSArs>`_.

    :returns:           A tuple with the first element being a list of indices 
                        representing the single solutions minimizing
                        the overall energy function.
                        The second element is the according energy value.          
