packages feed

dvda-0.2.2: Dvda/SymMonad.hs

{-# OPTIONS_GHC -Wall #-}
{-# Language TypeOperators #-}
{-# Language TypeFamilies #-}
{-# Language FlexibleInstances #-}
{-# Language FlexibleContexts #-}
{-# Language GADTs #-}
{-# Language DoAndIfThenElse #-}

module Dvda.SymMonad ( (:*)(..)
                     , MkFunGraph(..)
                     , node
                     , inputs
                     , inputs_
                     , outputs
                     , outputs_
                     , makeFunGraph
                     , runFunGraph
                     , rad
                     , getSensitivities
                     , recover
                     , fullShow
                     , fullShowNodes
                     , runDeriv
                     ) where

import Control.Monad ( foldM, liftM )
import Control.Monad.State ( State, get, put, runState )
import Data.Array.Repa ( DIM0, DIM1, DIM2, Z(..) )
import Data.Hashable ( Hashable )
import Data.Maybe ( fromJust )
import qualified Data.HashSet as HS
import qualified Data.IntMap as IM
import Numeric.LinearAlgebra ( Element, Vector, Matrix )
import qualified Numeric.LinearAlgebra as LA
-- import Debug.Trace

import Dvda.Dual ( Dual(..), dualPerturbation )
import Dvda.BinUn ( applyUnary, applyBinary )
import Dvda.Graph ( FunGraph(..), DynamicExpr(..), DvdaDim(..), insert, emptyFunGraph, fgLookup, fgExprFromKey )
import Dvda.Expr ( Expr(..), Const(..), Sym(..), dim )
import qualified Dvda.HashMap as HM

---- | take all sub expressions of an Expr and turn them into nodes
----   return an Expr that is just a ref
node :: (Hashable a, Eq a, Floating a, Num (Vector a), LA.Container Vector a, DvdaDim sh) => 
         Expr sh a -> State (FunGraph a b c) (Expr sh a)
node (EDimensionless _) = error "don't put EDimensionless in graph, ya goon"
node (EJacob _ _) = error "can't do node EJacob yet"
node e@(ERef _ _ _) = return e
node e@(EConst _) = return e
node e@(ESym _ (SymDependent _ _ dep)) = do
  _ <- node (ESym Z dep)
  insert e
node e@(ESym _ _) = insert e
node (EUnary op x') = do
  x <- node x'
  insert $ EUnary op x
node (EBinary op x' y') = do
  x <- node x'
  y <- node y'
  insert $ EBinary op x y
node (EScale x' y') = do
  x <- node x'
  y <- node y'
  insert $ EScale x y
node (EDeriv x_ arg_) = do
  x <- node x_
  arg <- node arg_
  outs <- rad x [arg]
  node (head outs)
node (EGrad x_ arg_) = do
  x <- node x_
  arg <- node arg_
  outs <- rad x [arg]
  node (head outs)


-- gradient of expression w.r.t. list of args
rad :: (Eq a, Floating a, Num (Vector a), Hashable a, LA.Container Vector a, DvdaDim sh0, DvdaDim sh) =>
       Expr sh0 a -> [Expr sh a] -> State (FunGraph a b c) [Expr sh a]
rad expr' args' = do
  expr <- node expr'
  args'' <- mapM node args'
  fg <- get

  let args = map (\(ERef sh _ k) -> fromJust $ fgExprFromKey sh k fg) args''
      argSet = HS.fromList (map makeDynamic args)

  sensitivities <- getSensitivities argSet expr (EConst (CSingleton (dim expr) 1))
  -- order inputs requested by user
  
  let getSens arg = case HM.lookup (makeDynamic arg) sensitivities of
        Just sens -> node $ fromDynamic (dim arg) sens
--        Nothing -> trace "WARNING: taking deriviative df/dx where f is not a function of x" $
--                   return $ EConst (CSingleton (dim arg) 0)
        Nothing -> return $ EConst (CSingleton (dim arg) 0)
  mapM getSens args


-- | combine two (DynamicExpr a, DynamicExpr a) hashmaps
-- if there is a conflict, add the two sensitivities together
unionWithPlus :: (Hashable a, Eq a, Num (Vector a), LA.Container Vector a, Floating a) =>
                 HM.HashMap (DynamicExpr a) (DynamicExpr a) -> HM.HashMap (DynamicExpr a) (DynamicExpr a)
                 -> State (FunGraph a b c) (HM.HashMap (DynamicExpr a) (DynamicExpr a))
unionWithPlus xs ys = foldM addCommon union0 commonDExprs
  where
    -- the gexprs that occur in both maps
    commonDExprs = HM.keys $ HM.intersection xs ys
    -- the initial union that needs conflicts fixed
    union0 = xs `HM.union` ys
    addCommon hm commonDExpr = do
      let xsens = fromJust $ HM.lookup commonDExpr xs
          ysens = fromJust $ HM.lookup commonDExpr ys
      xysens <- case (xsens,ysens) of
        (DynamicExpr0 x, DynamicExpr0 y) -> do
          ret <- node (x + y)
          return (makeDynamic ret)
        (DynamicExpr1 x, DynamicExpr1 y) -> do
          ret <- node (x + y)
          return (makeDynamic ret)
        (DynamicExpr2 x, DynamicExpr2 y) -> do
          ret <- node (x + y)
          return (makeDynamic ret)
        (_, _) -> error "unionWithPlus got different dimensions"
      return (HM.insert commonDExpr xysens hm)


lookupSymSet :: (Eq a, Hashable a, Element a, DvdaDim sh) =>
                Expr sh a -> State (FunGraph a b c) (Maybe (HS.HashSet (DynamicExpr a)))
lookupSymSet expr = do
  fg <- get
  case fgLookup expr fg of Just (_,symSet) -> return (Just symSet)
                           Nothing -> return Nothing

getSensitivities :: (Eq a, Floating a, Num (Vector a), Hashable a, LA.Container Vector a, DvdaDim sh) =>
                    HS.HashSet (DynamicExpr a) -> Expr sh a -> Expr sh a
                    -> State (FunGraph a b c) (HM.HashMap (DynamicExpr a) (DynamicExpr a))
getSensitivities _ (EGrad  _ _) _ = error "don't call getSensitivities on EGrad"
getSensitivities _ (EJacob _ _) _ = error "don't call getSensitivities on EJacob"
getSensitivities _ (EDeriv _ _) _ = error "don't call getSensitivities on EDeriv"
getSensitivities _ (EScale _ _) _ = error "cant' do getSensitivities on EScale yet (needs EinSum?)"
getSensitivities _ (EDimensionless _) _ = return HM.empty
getSensitivities _ (EConst _) _         = return HM.empty
getSensitivities args (ERef sh _ k) sens  = do
  fg <- get
  let expr = fromJust $ fgExprFromKey sh k fg
  getSensitivities args expr sens
getSensitivities args primal@(ESym sh (SymDependent name k dep')) sens = do
  let dprimal = makeDynamic primal
      primalMap =
        if HS.member dprimal args
        then HM.fromList [(dprimal, makeDynamic sens)]
        -- don't backprop if there aren't any interesting symbols farther in the tree
        else HM.empty

      dep = ESym sh dep'

  depSymSet <- liftM fromJust $ lookupSymSet dep

  let commonSyms = HS.intersection args depSymSet

  dependentMap <- case HS.size commonSyms of
    0 -> return HM.empty
    _ -> getSensitivities commonSyms dep (sens*primal')
      where
        primal' = ESym sh (SymDependent name (k+1) dep')

  return $ HM.union primalMap dependentMap
getSensitivities args primal@(ESym _ _) sens = do
  let dprimal = makeDynamic primal
  if HS.member dprimal args
  then return $ HM.fromList [(dprimal, makeDynamic sens)]
    -- don't backprop if there aren't any interesting symbols farther in the tree
  else return HM.empty

getSensitivities args (EUnary op g) sens = do
  symSetG <- liftM fromJust $ lookupSymSet g
  case HS.size (HS.intersection args symSetG) of
    -- don't backprop if there aren't any interesting symbols farther in the tree
    0 -> return HM.empty
    _ -> do
      let dfdg = dualPerturbation $ applyUnary op (Dual g 1)
      getSensitivities args g (sens*dfdg)
getSensitivities args (EBinary op g h) sens = do
  symSetG <- lookupSymSet g
  symSetH <- lookupSymSet h
  
  let dfdg = dualPerturbation $ applyBinary op (Dual g 1) (Dual h 0)
      dfdh = dualPerturbation $ applyBinary op (Dual g 0) (Dual h 1)
  
  gsens <- case liftM HS.size (liftM (HS.intersection args) symSetG) of
                Nothing -> return HM.empty
                Just 0 -> return HM.empty
                _ -> getSensitivities args g (sens*dfdg)
  hsens <- case liftM HS.size (liftM (HS.intersection args) symSetH) of
                Nothing -> return HM.empty
                Just 0 -> return HM.empty
                _ -> getSensitivities args h (sens*dfdh)
  unionWithPlus gsens hsens
--getSensitivities args (EScale g h) sens = do
--  symSetG <- lookupSymSet g
--  symSetH <- lookupSymSet h
--  
--  fg <- get
--  let dfdg = h
--      dfdh = g
--  
--  gsens <- case liftM HS.size (liftM (HS.intersection args) symSetG) of
--                Nothing -> return HM.empty
--                Just 0 -> return HM.empty
--                _ -> getSensitivities args g (sens*dfdg)
--  hsens <- case liftM HS.size (liftM (HS.intersection args) symSetH) of
--                Nothing -> return HM.empty
--                0 -> return HM.empty
--                _ -> getSensitivities args h (sens*dfdh)
--  unionWithPlus gsens hsens
--getSensitivities _ (EDeriv _ _) _ = error "don't call getSensitivities on EDeriv"
--getSensitivities _ (EGrad _ _) _  = error "don't call getSensitivities on EGrad"
--getSensitivities _ (EJacob _ _) _ = error "don't call getSensitivities on EJacob"
  


---------------------- heterogenous inputs/outputs ------------------
data a :* b = a :* b deriving Show
infixr 6 :*


---------------------------------- input/output class ---------------------------------------------
class MkFunGraph a where
  type NumT a
  type GenT a
  mkNodes :: a -> State (FunGraph (NumT a) b c) a

instance (Hashable a, Eq a, Floating a, Num (Vector a), LA.Container Vector a) =>
         MkFunGraph (Expr DIM0 a) where
  type NumT (Expr DIM0 a) = a
  type GenT (Expr DIM0 a) = a
  mkNodes = node

instance (Hashable a, Eq a, Floating a, Num (Vector a), LA.Container Vector a) =>
         MkFunGraph (Expr DIM1 a) where
  type NumT (Expr DIM1 a) = a
  type GenT (Expr DIM1 a) = Vector a
  mkNodes = node

instance (Hashable a, Eq a, Floating a, Num (Vector a), LA.Container Vector a) =>
         MkFunGraph (Expr DIM2 a) where
  type NumT (Expr DIM2 a) = a
  type GenT (Expr DIM2 a) = Matrix a
  mkNodes = node

instance (Hashable a, Eq a, Floating a, Num (Vector a), LA.Container Vector a, MkFunGraph (Expr sh a), DvdaDim sh) =>
         MkFunGraph [Expr sh a] where
  type NumT [Expr sh a] = a
  type GenT [Expr sh a] = [GenT (Expr sh a)]
  mkNodes = mapM node

instance (Hashable a, Eq a, Floating a, Num (Vector a), LA.Container Vector a, MkFunGraph (Expr sh a), DvdaDim sh) =>
         MkFunGraph [[Expr sh a]] where
  type NumT [[Expr sh a]] = a
  type GenT [[Expr sh a]] = [[GenT (Expr sh a)]]
  mkNodes = mapM (mapM node)

--instance (Show a, MkFunGraph a) => MkFunGraph [a] where
--  type NumT [a] = NumT a
--  type GenT [a] = [GenT a]
--  type KeyT [a] = [KeyT a]
--  mkNodes xs = do
--    (x',kxs) <- mapM mkNodes xs >>= (return . unzip)
--    return (x', concat kxs)

instance (MkFunGraph a, MkFunGraph b, NumT a ~ NumT b) => MkFunGraph (a :* b) where
  type NumT (a :* b) = NumT a
  type GenT (a :* b) = GenT a :* GenT b
  mkNodes (x :* y) = do
    x' <- mkNodes x
    y' <- mkNodes y
    return (x' :* y')

inputs :: MkFunGraph b => b -> State (FunGraph (NumT b) b c) b
inputs exprs_ = do
  exprs <- mkNodes exprs_
  FunGraph hm im _ outs <- get
  put $ FunGraph hm im exprs outs
  return exprs

outputs :: MkFunGraph c => c -> State (FunGraph (NumT c) b c) c
outputs exprs_ = do
  exprs <- mkNodes exprs_
  FunGraph hm im ins _ <- get
  put $ FunGraph hm im ins exprs
  return exprs

inputs_ :: MkFunGraph b => b -> State (FunGraph (NumT b) b c) ()
inputs_ exprs = do
  _ <- inputs exprs
  return ()

outputs_ :: MkFunGraph c => c -> State (FunGraph (NumT c) b c) ()
outputs_ exprs = do
  _ <- outputs exprs
  return ()

------------------ utility function -----------------
runFunGraph :: State (FunGraph a b c) d -> FunGraph a b c
runFunGraph f = snd $ runState f emptyFunGraph

makeFunGraph :: (MkFunGraph b, MkFunGraph c, NumT b ~ NumT c) =>
                b -> c -> FunGraph (NumT b) b c
makeFunGraph ins outs = runFunGraph $ do
  inputs_ ins
  outputs_ outs

-- | Show an Expr, looking up all ERefs
fullShow :: (Show a, Element a, DvdaDim sh) => FunGraph a b c -> Expr sh a -> String
fullShow fg = show . (recover fg)

fullShowNodes :: (Show a, Element a) => FunGraph a b c -> String
fullShowNodes fg@(FunGraph _ im _ _) =
  init $ unlines $ map (\(a,b) -> show a ++ ": " ++ (fullShow fg) (fromDynamic Z b)) (IM.toList im)

-- | Take a FunGraph and an expression and traverse the expression.
--   .
--   Each time an ERef is found, look it up in the FunGraph and continue traversal
recover :: DvdaDim sh => FunGraph a b c -> Expr sh a -> Expr sh a
recover fg (ERef sh _ k) = recover fg (fromJust $ fgExprFromKey sh k fg)
recover _ e@(EDimensionless _) = e
recover _ e@(ESym _ _) = e
recover _ e@(EConst _) = e
recover fg (EUnary op x) = EUnary op (recover fg x)
recover fg (EBinary op x y) = EBinary op (recover fg x) (recover fg y)
recover fg (EDeriv x y) = EDeriv (recover fg x) (recover fg y)
recover fg (EGrad  x y) = EGrad  (recover fg x) (recover fg y)
recover fg (EJacob  x y) = EJacob  (recover fg x) (recover fg y)
recover fg (EScale  x y) = EScale  (recover fg x) (recover fg y)

-- | "Pure" gradient which which runs rad and then calls recover to substitute values for ERefs
runDeriv :: (Eq a, Floating a, Num (Vector a), Hashable a, LA.Container Vector a, DvdaDim sh)
            => Expr sh a -> [Expr sh a] -> [Expr sh a]
runDeriv expr args = map (recover fg) deda
  where
    (deda, fg) = runState (rad expr args) emptyFunGraph