packages feed

numeric-optimization-0.1.1.0: test/Spec.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE LambdaCase #-}
import Test.Hspec

import Control.Exception
import Control.Monad
import Data.IORef
import Data.Vector.Storable (Vector)
import Numeric.LinearAlgebra (Matrix, (><))
import Numeric.Optimization
import IsClose


main :: IO ()
main = hspec $ do
  describe "minimize CGDescent" $ do
    when (isSupportedMethod CGDescent) $ do
      context "when given rosenbrock function" $
        it "returns the global optimum" $ do
          let prob = WithGrad rosenbrock rosenbrock'
          result <- minimize CGDescent def prob [-3,-4]
          resultSuccess result `shouldBe` True
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]
          assertAllClose (def :: Tol Double) (resultValue result) (func prob (resultSolution result))
          case resultGrad result of
            Nothing -> return ()
            Just g -> assertAllClose (def :: Tol Double) g (grad prob (resultSolution result))
          resultHessian result `shouldBe` Nothing
          resultHessianInv result `shouldBe` Nothing
          let stat = resultStatistics result
          totalIters stat `shouldSatisfy` (> 0)
          funcEvals stat `shouldSatisfy` (> 0)
          gradEvals stat `shouldSatisfy` (> 0)
          hessianEvals stat `shouldBe` 0

      context "when given paramsMaxIters" $
        it "stops iterations early" $ do
          let prob = WithGrad rosenbrock rosenbrock'
          result <- minimize CGDescent def{ paramsMaxIters = Just 2 } prob [1000, 1000]
          -- XXX: It seems that CG_DESCENT-C-3.0 report a number number of iterations that is 1 greater than the actual value
          totalIters (resultStatistics result) `shouldSatisfy` (\i -> 0 < i && i <= 2+1)
          resultSuccess result `shouldBe` False

      context "when given a function without gradient" $ do
        it "should throw GradUnavailable" $ do
          minimize CGDescent def rosenbrock [-3,-4] `shouldThrow` (GradUnavailable ==)

      context "when given a problem with bounds" $ do
        it "should throw UnsupportedProblem" $ do
          minimize CGDescent def (rosenbrock `WithGrad` rosenbrock' `WithBounds` [(-4,2), (-5,2)]) [-3,-4]
            `shouldThrow` (\case { UnsupportedProblem _ -> True; _ -> False })

  describe "minimize LBFGS" $ do
    when (isSupportedMethod LBFGS) $ do
      context "when given rosenbrock function" $
        it "returns the global optimum" $ do
          let prob = WithGrad rosenbrock rosenbrock'
          result <- minimize LBFGS def prob [-3,-4]
          resultSuccess result `shouldBe` True
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]
          assertAllClose (def :: Tol Double) (resultValue result) (func prob (resultSolution result))
          case resultGrad result of
            Nothing -> return ()
            Just g -> assertAllClose (def :: Tol Double) g (grad prob (resultSolution result))
          resultHessian result `shouldBe` Nothing
          resultHessianInv result `shouldBe` Nothing
          let stat = resultStatistics result
          totalIters stat `shouldSatisfy` (>0)
          funcEvals stat `shouldSatisfy` (>0)
          gradEvals stat `shouldSatisfy` (>0)
          hessianEvals stat `shouldBe` 0

      context "when given rosenbrock function with past" $
        it "returns the global optimum" $ do
          let prob = WithGrad rosenbrock rosenbrock'
          result <- minimize LBFGS def{ paramsPast = Just 1 } prob [-3,-4]
          resultSuccess result `shouldBe` True
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]
          assertAllClose (def :: Tol Double) (resultValue result) (func prob (resultSolution result))

      context "when given callback" $
        it "stops iterations early" $ do
          let prob = rosenbrock `WithGrad` rosenbrock'
          counter <- newIORef (0 :: Int)
          let callback x = do
                evaluate x
                cnt <- readIORef counter
                writeIORef counter (cnt + 1)
                return (cnt >= 2)
          result <- minimize LBFGS def{ paramsCallback = Just callback } prob [1000, 1000]
          totalIters (resultStatistics result) `shouldBe` 3  -- ???
          resultSuccess result `shouldBe` False

      context "when given a function without gradient" $ do
        it "should throw GradUnavailable" $ do
          minimize LBFGS def rosenbrock [-3,-4] `shouldThrow` (GradUnavailable ==)

      context "when given a problem with bounds" $ do
        it "should throw UnsupportedProblem" $ do
          minimize LBFGS def (rosenbrock `WithGrad` rosenbrock' `WithBounds` [(-4,2), (-5,2)]) [-3,-4]
            `shouldThrow` (\case { UnsupportedProblem _ -> True; _ -> False })

  describe "minimize LBFGSB" $ do
    when (isSupportedMethod LBFGSB) $ do
      context "when given rosenbrock function" $
        it "returns the global optimum" $ do
          let prob = rosenbrock `WithGrad` rosenbrock'
          result <- minimize LBFGSB def prob [-3,-4]
          resultSuccess result `shouldBe` True
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]
          assertAllClose (def :: Tol Double) (resultValue result) (func prob (resultSolution result))
          case resultGrad result of
            Nothing -> return ()
            Just g -> assertAllClose (def :: Tol Double) g (grad prob (resultSolution result))
          evaluate $ resultHessian result
          evaluate $ resultHessianInv result
          let stat = resultStatistics result
          totalIters stat `shouldSatisfy` (>0)
          funcEvals stat `shouldSatisfy` (>0)
          gradEvals stat `shouldSatisfy` (>0)
          hessianEvals stat `shouldBe` 0

      context "when given rosenbrock function with ftol (low accuracy)" $
        it "returns a solution not close enough to global optimum" $ do
          let prob = rosenbrock `WithGrad` rosenbrock'
              eps = 2.220446049250313e-16
          result <- minimize LBFGSB def{ paramsFTol = Just (1e12 * eps) } prob [-3,-4]
          resultSuccess result `shouldBe` True
          allClose (def :: Tol Double) (resultSolution result) [1,1] `shouldBe` False

      context "when given rosenbrock function with bounds" $
        it "returns the global optimum" $ do
          let prob = rosenbrock `WithGrad` rosenbrock' `WithBounds` [(-4,2), (-5,2)]
          result <- minimize LBFGSB def prob [-3,-4]
          resultSuccess result `shouldBe` True
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]

      context "when given rosenbrock function with bounds (-infinity, +infinity)" $
        it "returns the global optimum" $ do
          let prob = rosenbrock `WithGrad` rosenbrock' `WithBounds` boundsUnconstrained 2
          result <- minimize LBFGSB def prob [-3,-4]
          resultSuccess result `shouldBe` True
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]

      context "when given paramsMaxIters" $
        it "stops iterations early" $ do
          let prob = WithGrad rosenbrock rosenbrock'
          result <- minimize LBFGSB def{ paramsMaxIters = Just 2 } prob [1000, 1000]
          totalIters (resultStatistics result) `shouldSatisfy` (\i -> 0 < i && i <= 2)
          resultSuccess result `shouldBe` False

      context "when given a function without gradient" $ do
        it "should throw GradUnavailable" $ do
          minimize LBFGSB def rosenbrock [-3,-4] `shouldThrow` (GradUnavailable ==)

  describe "minimize Newton" $ do
    when (isSupportedMethod Newton) $ do
      context "when given rosenbrock function" $
        it "returns the global optimum" $ do
          let prob = rosenbrock `WithGrad` rosenbrock' `WithHessian` rosenbrock''
          result <- minimize Newton def prob [-3,-4]
          resultSuccess result `shouldBe` True
          totalIters (resultStatistics result) `shouldSatisfy` (> 0)
          assertAllClose (def :: Tol Double) (resultSolution result) [1,1]
          assertAllClose (def :: Tol Double) (resultValue result) (func prob (resultSolution result))
          case resultGrad result of
            Nothing -> return ()
            Just g -> assertAllClose (def :: Tol Double) g (grad prob (resultSolution result))
          case resultHessian result of
            Nothing -> return ()
            Just h -> assertAllClose (def :: Tol Double) h (hessian prob (resultSolution result))
          let stat = resultStatistics result
          totalIters stat `shouldSatisfy` (>0)
          funcEvals stat `shouldSatisfy` (>0)
          gradEvals stat `shouldSatisfy` (>0)
          hessianEvals stat `shouldSatisfy` (>0)

      context "when given paramsMaxIters" $
        it "stops iterations early" $ do
          let prob = rosenbrock `WithGrad` rosenbrock' `WithHessian` rosenbrock''
          result <- minimize Newton def{ paramsMaxIters = Just 2 } prob [1000, 1000]
          totalIters (resultStatistics result) `shouldSatisfy` (\i -> 0 < i && i <= 2)
          resultSuccess result `shouldBe` False
          assertAllClose (def :: Tol Double) (resultValue result) (func prob (resultSolution result))
          case resultGrad result of
            Nothing -> return ()
            Just g -> assertAllClose (def :: Tol Double) g (grad prob (resultSolution result))
          case resultHessian result of
            Nothing -> return ()
            Just h -> assertAllClose (def :: Tol Double) h (hessian prob (resultSolution result))

      context "when given callback" $
        it "stops iterations early" $ do
          let prob = rosenbrock `WithGrad` rosenbrock' `WithHessian` rosenbrock''
          counter <- newIORef (0 :: Int)
          let callback x = do
                evaluate x
                cnt <- readIORef counter
                writeIORef counter (cnt + 1)
                return (cnt >= 2)
          result <- minimize Newton def{ paramsCallback = Just callback } prob [1000, 1000]
          resultSuccess result `shouldBe` False
          let stat = resultStatistics result
          totalIters stat `shouldBe` 2
          funcEvals stat `shouldSatisfy` (> 0)
          gradEvals stat `shouldSatisfy` (> 0)
          hessianEvals stat `shouldSatisfy` (> 0)

      context "when given a function without gradient" $ do
        it "should throw GradUnavailable" $ do
          minimize Newton def (rosenbrock `WithHessian` rosenbrock'') [-3,-4] `shouldThrow` (GradUnavailable ==)

      context "when given a function without Hessian" $ do
        it "should throw HessianUnavailable" $ do
          minimize Newton def (rosenbrock `WithGrad` rosenbrock') [-3,-4] `shouldThrow` (HessianUnavailable ==)

      context "when given a problem with bounds" $ do
        it "should throw UnsupportedProblem" $ do
          minimize Newton def (rosenbrock `WithGrad` rosenbrock' `WithHessian` rosenbrock'' `WithBounds` [(-4,2), (-5,2)]) [-3,-4]
            `shouldThrow` (\case { UnsupportedProblem _ -> True; _ -> False })

-- https://en.wikipedia.org/wiki/Rosenbrock_function
rosenbrock :: Vector Double -> Double
rosenbrock [x,y] = sq (1 - x) + 100 * sq (y - sq x)

rosenbrock' :: Vector Double -> Vector Double
rosenbrock' [x,y] =
  [ 2 * (1 - x) * (-1) + 100 * 2 * (y - sq x) * (-2) * x
  , 100 * 2 * (y - sq x)
  ]

rosenbrock'' :: Vector Double -> Matrix Double
rosenbrock'' [x,y] =
  (2><2)
  [ 2 + 100 * 2 * (-2) * ((y - sq x) + (x * (-2) * x)), 100 * 2 * (-2) * x
  , 100 * 2 * (-2) * x, 100 * 2
  ]

sq :: Floating a => a -> a
sq x = x ** 2