module Optimization.Constrained.ProjectedSubgradient
    ( -- * Projected subgradient method
      projSubgrad
    , linearProjSubgrad
      -- * Step schedules
    , StepSched
    , optimalStepSched
    , constStepSched
    , sqrtKStepSched
    , invKStepSched
      -- * Linear constraints
    , Constraint(..)
    , linearProjection
    ) where

import Linear
import Data.Traversable
import Data.Function (on)
import Data.List (maximumBy)

-- | A step size schedule
-- A list of functions (one per step) which, given a function's
-- gradient and value, will provide a size for the next step
type StepSched f a = [f a -> a -> a]

-- | @projSubgrad stepSizes proj a b x0@ minimizes the objective @A
-- x - b@ potentially projecting iterates into a feasible space with
-- @proj@ with the given step-size schedule
projSubgrad :: (Additive f, Traversable f, Metric f, Ord a, Fractional a)
            => StepSched f a  -- ^ A step size schedule
            -> (f a -> f a)   -- ^ Function projecting into the feasible space
            -> (f a -> f a)   -- ^ Gradient of objective function
            -> (f a -> a)     -- ^ The objective function
            -> f a            -- ^ Initial solution
            -> [f a]
projSubgrad stepSizes proj df f = go stepSizes
  where go (alpha:rest) x0 =
            let p = negated $ df x0
                step = alpha (df x0) (f x0)
                x1 = proj $ x0 ^+^ step *^ p
            in x1 : go rest x1
        go [] _ = []

-- | @linearProjSubgrad stepSizes proj a b x0@ minimizes the objective @A
-- x - b@ potentially projecting iterates into a feasible space with
-- @proj@ with the given step-size schedule
linearProjSubgrad :: (Additive f, Traversable f, Metric f, Ord a, Fractional a)
                  => StepSched f a  -- ^ A step size schedule
                  -> (f a -> f a)   -- ^ Function projecting into the feasible space
                  -> f a            -- ^ Coefficient vector @A@ of objective
                  -> a              -- ^ Constant @b@ of objective
                  -> f a            -- ^ Initial solution
                  -> [f a]
linearProjSubgrad stepSizes proj a b = go stepSizes
  where go (alpha:rest) x0 =
            let p = negated $ df x0
                step = alpha a (f x0)
                x1 = proj $ x0 ^+^ step *^ p
            in x1 : go rest x1
        go [] _ = []
        df _ = a
        f x = a `dot` x - b

-- | The optimal step size schedule when the optimal value of the
-- objective is known
optimalStepSched :: (Fractional a, Metric f)
                 => a    -- ^ The optimal value of the objective
                 -> StepSched f a
optimalStepSched fStar =
    repeat $ \gk fk->(fk - fStar) / quadrance gk

-- | Constant step size schedule
constStepSched :: a    -- ^ The step size
               -> StepSched f a
constStepSched gamma =
    repeat $ \_ _ -> gamma

-- | A non-summable step size schedule
sqrtKStepSched :: Floating a
               => a       -- ^ The size of the first step
               -> StepSched f a
sqrtKStepSched gamma =
    map (\k _ _ -> gamma / sqrt (fromIntegral k)) [0..]

-- | A square-summable step size schedule
invKStepSched :: Fractional a
              => a        -- ^ The size of the first step
              -> StepSched f a
invKStepSched gamma =
    map (\k _ _ -> gamma / fromIntegral k) [0..]

-- | A linear constraint. For instance, @Constr LT 2 (V2 1 3)@ defines
-- the constraint @x_1 + 3 x_2 <= 2@
data Constraint f a = Constr Ordering a (f a)
                    deriving (Show)

-- | Project onto a the space of feasible solutions defined by a set
-- of linear constraints
linearProjection :: (Fractional a, Ord a, RealFloat a, Metric f)
                 => [Constraint f a] -- ^ A set of linear constraints
                 -> f a -> f a
linearProjection constraints x =
    case unmet of
      []   -> x
      _    -> linearProjection constraints $ fixConstraint x
              $ maximumBy (flip compare `on` (`ap` x)) unmet
  where unmet = filter (not . met x) constraints
        ap (Constr _ b a) c = a `dot` c - b
        met c (Constr t a constr) = let y = constr `dot` c - a
                                    in case t of
                                       EQ -> abs y < 1e-4
                                       GT -> y >= 0 || abs y < 1e-4
                                       LT -> y <= 0 || abs y < 1e-4
        fixConstraint c (Constr _ b a) = c ^-^ (a `dot` c - b) *^ a ^/ quadrance a
