------------------------------------------------------------------------
-- |
-- Module      :  Data.Datamining.Clustering.SOMInternal
-- Copyright   :  (c) Amy de Buitléir 2012-2013
-- License     :  BSD-style
-- Maintainer  :  amy@nualeargais.ie
-- Stability   :  experimental
-- Portability :  portable
--
-- A module containing private @SOM@ internals. Most developers should
-- use @SOM@ instead. This module is subject to change without notice.
--
------------------------------------------------------------------------
{-# LANGUAGE TypeFamilies, FlexibleContexts, FlexibleInstances,
    MultiParamTypeClasses, DeriveGeneric #-}

module Data.Datamining.Clustering.SOMInternal where

import qualified Data.Foldable as F (Foldable, foldr)
import Data.List (foldl', minimumBy)
import Data.Ord (comparing)
import qualified Math.Geometry.Grid as G (Grid(..))
import qualified Math.Geometry.GridMap as GM (GridMap(..))
import Data.Datamining.Pattern (Pattern(..))
import Data.Datamining.Clustering.Classifier(Classifier(..))
import GHC.Generics (Generic)
import Prelude hiding (lookup)

class LearningFunction f where
  type LearningRate f
  rate :: f -> Int -> Int -> (LearningRate f)

-- | A typical learning function for classifiers.
--   @'DecayingGaussian' r0 rf w0 wf tf@ returns a bell curve-shaped
--   function. At time zero, the maximum learning rate (applied to the
--   BMU) is @r0@, and the neighbourhood width is @w0@. Over time the
--   bell curve shrinks and the learning rate tapers off, until at time
--   @tf@, the maximum learning rate (applied to the BMU) is @rf@,
--   and the neighbourhood width is @wf@. Normally the parameters
--   should be chosen such that:
--
--   * 0 < rf << r0 < 1
--
--   * 0 < wf << w0
--
--   * 0 < tf
--
--   where << means "is much smaller than" (not the Haskell @<<@
--   operator!)
data DecayingGaussian a = DecayingGaussian a a a a Int
  deriving (Eq, Show, Generic)

instance (Floating a, Fractional a, Num a) =>
         LearningFunction (DecayingGaussian a) where
  type LearningRate (DecayingGaussian a) = a
  rate (DecayingGaussian r0 rf w0 wf tf) t d = r * exp (-(d'*d')/(w*w))
    where a = t'/tf'
          r = r0 * ((rf/r0)**a)
          w = w0 * ((wf/w0)**a)
          t' = fromIntegral t
          tf' = fromIntegral tf
          d' = fromIntegral d

-- | A learning function that only updates the BMU and has a constant
--   learning rate.
data StepFunction a = StepFunction a deriving (Eq, Show, Generic)

instance (Fractional a) => LearningFunction (StepFunction a) where
  type LearningRate (StepFunction a) = a
  rate (StepFunction r) _ d = if d == 0 then r else 0.0

-- | A learning function that updates all nodes with the same, constant
--   learning rate. This can be useful for testing.
data ConstantFunction a = ConstantFunction a deriving (Eq, Show, Generic)

instance (Fractional a) => LearningFunction (ConstantFunction a) where
  type LearningRate (ConstantFunction a) = a
  rate (ConstantFunction r) _ _ = r

-- | A Self-Organising Map (SOM).
--
--   Although @SOM@ implements @GridMap@, most users will only need the
--   interface provided by @Data.Datamining.Clustering.Classifier@. If
--   you chose to use the @GridMap@ functions, please note:
--
--   1. The functions @adjust@, and @adjustWithKey@ do not increment the
--      counter. You can do so manually with @incrementCounter@.
--
--   2. The functions @map@ and @mapWithKey@ are not implemented (they
--      just return an @error@). It would be problematic to implement
--      them because the input SOM and the output SOM would have to have
--      the same @Metric@ type.
data SOM f gm k p = SOM
  {
    sGridMap :: gm p,
    sLearningFunction :: f,
    sCounter :: Int
  } deriving (Eq, Show, Generic)

instance (F.Foldable gm) => F.Foldable (SOM f gm k) where
  foldr f x g = F.foldr f x (sGridMap g)

instance (G.Grid (gm p)) => G.Grid (SOM f gm k p) where
  type Index (SOM f gm k p) = G.Index (gm p)
  type Direction (SOM f gm k p) = G.Direction (gm p)
  indices = G.indices . sGridMap
  distance = G.distance . sGridMap
  neighbours = G.neighbours . sGridMap
  contains = G.contains . sGridMap
  viewpoint = G.viewpoint . sGridMap
  directionTo = G.directionTo . sGridMap
  tileCount = G.tileCount . sGridMap
  null = G.null . sGridMap
  nonNull = G.nonNull . sGridMap

instance (F.Foldable gm, GM.GridMap gm p, G.Grid (GM.BaseGrid gm p)) => GM.GridMap (SOM f gm k) p where
  type BaseGrid (SOM f gm k) p = GM.BaseGrid gm p
  toGrid = GM.toGrid . sGridMap
  toMap = GM.toMap . sGridMap
  mapWithKey = error "Not implemented"
  adjustWithKey f k s = s { sGridMap=gm' }
    where gm = sGridMap s
          gm' = GM.adjustWithKey f k gm

currentLearningFunction
  :: (LearningFunction f, LearningRate f ~ Metric p)
    => SOM f gm k p -> (Int -> Metric p)
currentLearningFunction s = (rate . sLearningFunction $ s) (sCounter s)

-- | Extracts the grid and current models from the SOM.
toGridMap :: GM.GridMap gm p => SOM f gm k p -> gm p
toGridMap = sGridMap

adjustNode
  :: (Pattern p, G.Grid g, k ~ G.Index g) =>
     g -> (Int -> Metric p) -> p -> k -> k -> p -> p
adjustNode g f target bmu k = makeSimilar target (f d)
  where d = G.distance g bmu k

-- | Trains the specified node and the neighbourood around it to better
--   match a target.
--   Most users should use @train@, which automatically determines
--   the BMU and trains it and its neighbourhood.
trainNeighbourhood
  :: (Pattern p, G.Grid (gm p), GM.GridMap gm p,
      G.Index (GM.BaseGrid gm p) ~ G.Index (gm p), LearningFunction f,
      LearningRate f ~ Metric p) =>
     SOM f gm k p -> G.Index (gm p) -> p -> SOM f gm k p
trainNeighbourhood s bmu target = s { sGridMap=gm' }
  where gm = sGridMap s
        gm' = GM.mapWithKey (adjustNode gm f target bmu) gm
        f = currentLearningFunction s

incrementCounter :: SOM f gm k p -> SOM f gm k p
incrementCounter s = setCounter (sCounter s + 1) s

counter :: SOM f gm k p -> Int
counter = sCounter

setCounter :: Int -> SOM f gm k p -> SOM f gm k p
setCounter k s = s { sCounter = k }

justTrain
  :: (Ord (Metric p), Pattern p, G.Grid (gm p),
      GM.GridMap gm (Metric p), GM.GridMap gm p,
      G.Index (GM.BaseGrid gm (Metric p)) ~ G.Index (gm p),
      G.Index (GM.BaseGrid gm p) ~ G.Index (gm p), LearningFunction f,
      LearningRate f ~ Metric p) =>
     SOM f gm k p -> p -> SOM f gm k p
justTrain s p = trainNeighbourhood s bmu p
  where ds = GM.toList . GM.map (p `difference`) . sGridMap $ s
        bmu = fst . minimumBy (comparing snd) $ ds

instance
  (GM.GridMap gm p, k ~ G.Index (GM.BaseGrid gm p), Pattern p,
  G.Grid (gm p), GM.GridMap gm (Metric p), k ~ G.Index (gm p),
  k ~ G.Index (GM.BaseGrid gm (Metric p)), Ord (Metric p),
  LearningFunction f, LearningRate f ~ Metric p) =>
    Classifier (SOM f gm) k p where
  toList = GM.toList . sGridMap
  numModels = G.tileCount . sGridMap
  models = GM.elems . sGridMap
  differences s p = GM.toList . GM.map (p `difference`) . sGridMap $ s
  trainBatch s = incrementCounter . foldl' justTrain s
  reportAndTrain s p = (bmu, ds, incrementCounter s')
    where ds = differences s p
          bmu = fst . minimumBy (comparing snd) $ ds
          s' = trainNeighbourhood s bmu p


-- -- | Creates a classifier with a default (bell-shaped) learning
-- --   function. Usage is @'defaultSOM' gm r0 rf w0 wf tf@, where:
-- --
-- --   [@gm@] The geometry and initial models for this classifier.
-- --   A reasonable choice here is @'lazyGridMap' g ps@, where @g@ is a
-- --   @'HexHexGrid'@, and @ps@ is a set of random patterns.
-- --
-- --   [@r0@] See description in @'decayingGaussian2'@.
-- --
-- --   [@rf@] See description in @'decayingGaussian2'@.
-- --
-- --   [@w0@] See description in @'decayingGaussian2'@.
-- --
-- --   [@wf@] See description in @'decayingGaussian2'@.
-- --
-- --   [@tf@] See description in @'decayingGaussian2'@.
-- defaultSOM
--   :: Floating (Metric p) => gm p -> Metric p -> Metric p -> Metric p ->
--      Metric p -> Int -> SOM f gm k p
-- defaultSOM gm r0 rf w0 wf tf =
--   SOM {
--         sGridMap=gm,
--         sLearningFunction=DecayingGaussian r0 rf w0 wf tf,
--         sCounter=0
--       }

-- -- | Creates a classifier with a custom learning function.
-- --   Usage is @'customSOM' gm g@, where:
-- --
-- --   [@gm@] The geometry and initial models for this classifier.
-- --   A reasonable choice here is @'lazyGridMap' g ps@, where @g@ is a
-- --   @'HexHexGrid'@, and @ps@ is a set of random patterns.
-- --
-- --   [@f@] A function used to adjust the models in the classifier.
-- --   This function will be invoked with two parameters.
-- --   The first parameter will indicate how many patterns (or pattern
-- --   batches) have previously been presented to this classifier.
-- --   Typically this is used to make the learning rate decay over time.
-- --   The second parameter to the function is the grid distance from
-- --   the node being updated to the BMU (Best Matching Unit).
-- --   The output is the learning rate for that node (the amount by
-- --   which the node's model should be updated to match the target).
-- --   The learning rate should be between zero and one.
-- customSOM
--   :: (LearningFunction f, LearningRate f ~ Metric p)
--     => gm p -> f -> SOM f gm k p
-- customSOM gm f =
--   SOM {
--         sGridMap=gm,
--         sLearningFunction=f,
--         sCounter=0
--       }