// Copyright (C) 2010  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_LINEAR_MANIFOLD_ReGULARIZER_Hh_
#define DLIB_LINEAR_MANIFOLD_ReGULARIZER_Hh_
#include "linear_manifold_regularizer_abstract.h"
#include <limits>
#include <vector>
#include "../serialize.h"
#include "../matrix.h"
namespace dlib
{
    namespace impl
    {
        class undirected_adjacency_list
        {
            /*!
                WHAT THIS OBJECT REPRESENTS
                    This object is simply a tool for turning a vector of sample_pair objects
                    into an adjacency list with floating point weights on each edge.  
            !*/
        public:
            undirected_adjacency_list (
            )
            {
                _size = 0;
                sum_edge_weights = 0;
            }
            struct neighbor 
            {
                neighbor(unsigned long idx, double w):index(idx), weight(w) {}
                neighbor():index(0), weight(0) {}
                unsigned long index;
                double weight;
            };
            typedef std::vector<neighbor>::const_iterator const_iterator;
            size_t size (
            ) const
            /*!
                ensures
                    - returns the number of vertices in this graph
            !*/
            {
                return _size;
            }
            const_iterator begin(
                unsigned long idx
            ) const
            /*!
                requires
                    - idx < size()
                ensures
                    - returns an iterator that points to the first neighbor of 
                      the idx'th vertex.
            !*/
            {
                return blocks[idx];
            }
            const_iterator end(
                unsigned long idx
            ) const
            /*!
                requires
                    - idx < size()
                ensures
                    - returns an iterator that points one past the last neighbor
                      of the idx'th vertex.
            !*/
            {
                return blocks[idx+1];
            }
            template <typename vector_type, typename weight_function_type>
            void build (
                const vector_type& edges,
                const weight_function_type& weight_funct
            ) 
            /*!
                requires
                    - vector_type == a type with an interface compatible with std::vector and 
                      it must in turn contain objects with an interface compatible with dlib::sample_pair
                    - edges.size() > 0
                    - contains_duplicate_pairs(edges) == false
                    - weight_funct(edges[i]) must be a valid expression that evaluates to a
                      floating point number >= 0
                ensures
                    - #size() == one greater than the max index in edges.
                    - builds the adjacency list so that it contains all the given edges.
                    - The weight in each neighbor is set to the output of the weight_funct()
                      for the associated edge.
            !*/
            {
                // Figure out how many neighbors each sample ultimately has.  We do this so 
                // we will know how much space to allocate in the data vector.
                std::vector<unsigned long> num_neighbors;
                num_neighbors.reserve(edges.size());
                for (unsigned long i = 0; i < edges.size(); ++i)
                {
                    // make sure num_neighbors is always big enough 
                    const unsigned long min_size = std::max(edges[i].index1(), edges[i].index2())+1;
                    if (num_neighbors.size() < min_size)
                        num_neighbors.resize(min_size,  0);
                    num_neighbors[edges[i].index1()] += 1;
                    num_neighbors[edges[i].index2()] += 1;
                }
                _size = num_neighbors.size();
                // Now setup the iterators in blocks.  Also setup a version of blocks that holds
                // non-const iterators so we can use it below when we populate data.
                std::vector<std::vector<neighbor>::iterator> mutable_blocks;
                data.resize(edges.size()*2); // each edge will show up twice 
                blocks.resize(_size + 1);
                blocks[0] = data.begin();
                mutable_blocks.resize(_size + 1);
                mutable_blocks[0] = data.begin();
                for (unsigned long i = 0; i < num_neighbors.size(); ++i)
                {
                    blocks[i+1]         = blocks[i]         + num_neighbors[i];
                    mutable_blocks[i+1] = mutable_blocks[i] + num_neighbors[i];
                }
                sum_edge_weights = 0;
                // finally, put the edges into data
                for (unsigned long i = 0; i < edges.size(); ++i)
                {
                    const double weight = weight_funct(edges[i]);
                    sum_edge_weights += weight;
                    // make sure requires clause is not broken
                    DLIB_ASSERT(weight >= 0,
                        "\t void linear_manifold_regularizer::build()"
                        << "\n\t You supplied a weight_funct() that generated a negative weight."
                        << "\n\t weight: " << weight 
                        );
                    *mutable_blocks[edges[i].index1()]++ = neighbor(edges[i].index2(), weight);
                    *mutable_blocks[edges[i].index2()]++ = neighbor(edges[i].index1(), weight);
                }
            }
            double sum_of_edge_weights (
            ) const
            {
                return sum_edge_weights;
            }
        private:
            /*!
                INITIAL VALUE
                    - _size == 0
                    - data.size() == 0
                    - blocks.size() == 0
                    - sum_edge_weights == 0
                CONVENTION
                    - size() == _size
                    - blocks.size() == _size + 1
                    - sum_of_edge_weights() == sum_edge_weights
                    - blocks == a vector of iterators that point into data.  
                      For all valid i:
                        - The iterator range [blocks[i], blocks[i+1]) contains all the edges
                          for the i'th node in the graph
            !*/
            std::vector<neighbor> data;
            std::vector<const_iterator> blocks; 
            unsigned long _size;
            double sum_edge_weights;
        };
    }
// ----------------------------------------------------------------------------------------
    template <
        typename matrix_type
        >
    class linear_manifold_regularizer
    {
    public:
        typedef typename matrix_type::mem_manager_type mem_manager_type;
        typedef typename matrix_type::type scalar_type;
        typedef typename matrix_type::layout_type layout_type;
        typedef matrix<scalar_type,0,0,mem_manager_type,layout_type> general_matrix;
        template <
            typename vector_type1, 
            typename vector_type2, 
            typename weight_function_type
            >
        void build (
            const vector_type1& samples,
            const vector_type2& edges,
            const weight_function_type& weight_funct
        )
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(edges.size() > 0 &&
                        contains_duplicate_pairs(edges) == false &&
                        max_index_plus_one(edges) <= samples.size(),
                "\t void linear_manifold_regularizer::build()"
                << "\n\t Invalid inputs were given to this function."
                << "\n\t edges.size():                    " << edges.size()
                << "\n\t samples.size():                  " << samples.size()
                << "\n\t contains_duplicate_pairs(edges): " << contains_duplicate_pairs(edges) 
                << "\n\t max_index_plus_one(edges):       " << max_index_plus_one(edges) 
                );
            impl::undirected_adjacency_list graph;
            graph.build(edges, weight_funct);
            sum_edge_weights = graph.sum_of_edge_weights();
            make_mr_matrix(samples, graph);
        }
        long dimensionality (
        ) const { return reg_mat.nr(); }
        general_matrix get_transformation_matrix (
            scalar_type intrinsic_regularization_strength
        ) const
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(intrinsic_regularization_strength >= 0,
                "\t matrix linear_manifold_regularizer::get_transformation_matrix()"
                << "\n\t This value must not be negative"
                << "\n\t intrinsic_regularization_strength: " << intrinsic_regularization_strength 
                );
            if (dimensionality() == 0)
                return general_matrix();
            // This isn't how it's defined in the referenced paper but normalizing these kinds of
            // sums is typical of most machine learning algorithms.  Moreover, doing this makes
            // the argument to this function more invariant to the size of the edge set.  So it
            // should make it easier for the user.
            intrinsic_regularization_strength /= sum_edge_weights;
            return inv_lower_triangular(chol(identity_matrix<scalar_type>(reg_mat.nr()) + intrinsic_regularization_strength*reg_mat));
        }
    private:
        template <typename vector_type>
        void make_mr_matrix (
            const vector_type& samples,
            const impl::undirected_adjacency_list& graph
        )
        /*!
            requires
                - samples.size() == graph.size()
            ensures
                - computes trans(X)*lap(graph)*X where X is the data matrix 
                  (i.e. the matrix that contains all the samples in its rows)
                  and lap(graph) is the laplacian matrix of the graph.  The
                  resulting matrix is stored in reg_mat.
        !*/
        {
            const unsigned long dims = samples[0].size();
            reg_mat.set_size(dims,dims);
            reg_mat = 0;
            typename impl::undirected_adjacency_list::const_iterator beg, end;
            // loop over the columns of the X matrix
            for (unsigned long d = 0; d < dims; ++d)
            {
                // loop down the row of X
                for (unsigned long i = 0; i < graph.size(); ++i)
                {
                    beg = graph.begin(i);
                    end = graph.end(i);
                    // if this node in the graph has any neighbors
                    if (beg != end)
                    {
                        double weight_sum = 0;
                        double val = 0;
                        for (; beg != end; ++beg)
                        {
                            val -= beg->weight * samples[beg->index](d);
                            weight_sum += beg->weight;
                        }
                        val += weight_sum * samples[i](d);
                        for (unsigned long j = 0; j < dims; ++j)
                        {
                            reg_mat(d,j) += val*samples[i](j);
                        }
                    }
                }
            }
        }
        general_matrix reg_mat;
        double sum_edge_weights;
    };
}
// ----------------------------------------------------------------------------------------
#endif // DLIB_LINEAR_MANIFOLD_ReGULARIZER_Hh_