packages feed

srtree-3.0.0.0: test/Spec.hs

import Test.HUnit
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Storable as VS
import Data.SRTree.Internal
import Data.SRTree.Recursion (Fix)
import Data.SRTree.Eval (compile)
import Algorithm.SRTree.AD.Unboxed (CompiledTree, compileTree, compileTreeMulti, evalGrad, evalGradVec, evalGradMulti)
import qualified EqSatTests
import qualified StoreTests
import Data.SRTree.Random (randomTree, tossBiased, randomFrom)
import System.Random (mkStdGen)
import Control.Monad.State.Strict (evalStateT)
import Data.SRTree.Datasets (loadDataset)
import Control.Monad (forM_)

-- Small epsilon compare for Doubles
eps :: Double
eps = 1e-9

approxEqual :: [Double] -> [Double] -> Bool
approxEqual a b = and $ zipWith (\x y -> abs (x - y) < eps) a b

test_compile :: Test
test_compile = TestCase $ do
  let xss = [VU.fromList [1.0, 2.0, 3.0]]
      tree = var 0 * param 0 + param 1
      theta = VU.fromList [2.0, 0.5]
      yhat = compile xss tree theta
      got = VU.toList yhat
      expected = [2.5, 4.5, 6.5]
  assertBool ("compile produced " ++ show got ++ " expected " ++ show expected) (approxEqual got expected)

-- Gradient correctness: the compact ctStatic layout must agree with finite
-- differences (objective) and with the row-fused `evalGrad` backend across
-- the vectorized `evalGradVec` and chunked `evalGradMulti` paths.
test_grad :: Test
test_grad = TestCase $ do
  let xss = [ VU.fromList [1.0, 2.0, 3.0, 4.0]
            , VU.fromList [0.5, 1.5, 2.5, 3.5]
            , VU.fromList [2.0, 1.0, 0.5, 0.25] ]
      y   = VU.fromList [3.1, 5.2, 7.3, 9.4]
      -- ((x0 + t0) * exp(x1)) / (x2 + t1)  -- mixes static and dynamic subtrees
      tree = (var 0 + param 0) * exp (var 1) / (var 2 + param 1)
      theta = VS.fromList [1.0, 0.5]
      ct   = compileTree xss y Nothing tree
      cts  = compileTreeMulti xss y Nothing tree
      (f0, g0) = evalGrad ct theta
      (f1, g1) = evalGradVec ct theta
      (f2, g2) = evalGradMulti cts theta
      -- finite-difference gradient
      h  = 1e-6
      gfd = VS.toList $ VS.generate (VS.length theta) $ \i ->
              let e    = VS.fromList (map (\j -> if j == i then h else 0) [0 .. VS.length theta - 1])
                  (fp, _) = evalGradVec ct (VS.zipWith (+) theta e)
                  (fm, _) = evalGradVec ct (VS.zipWith (-) theta e)
              in (fp - fm) / (2 * h)
  assertBool "evalGradVec objective != evalGrad"   (abs (f1 - f0) < 1e-6)
  assertBool "evalGradMulti objective != evalGrad" (abs (f2 - f0) < 1e-6)
  assertBool "evalGradVec gradient != finite diff"
    (and (zipWith (\a b -> abs (a - b) < 1e-4) (VS.toList g1) gfd))
  assertBool "evalGrad gradient != finite diff"
    (and (zipWith (\a b -> abs (a - b) < 1e-4) (VS.toList g0) gfd))

test_benchgrad :: Test
test_benchgrad = TestCase $ do
  let genTerm    = do coin <- tossBiased 0.4
                      if coin then randomFrom [Fix $ Var ix | ix <- [0..8]] else randomFrom [Fix $ Param ix | ix <- [0..9]]
      genNonTerm = randomFrom [Bin Add () (), Bin Sub () (), Bin Mul () (), Uni LogAbs (), Uni SqrtAbs ()]
      genMultipleTrees 0 = pure []
      genMultipleTrees n = do
          t  <- randomTree 5 10 150 genTerm genNonTerm False
          ts <- genMultipleTrees (n-1)
          pure (t:ts)
      g = mkStdGen 42
  trees' <- evalStateT (genMultipleTrees 5) g
  ((dataset, y, _, _), _, _, _) <- loadDataset "data.tsv" True
  let thetaU = VU.fromList [1.0, 0.5, 0.2, 0.3, 0.1, 0.5, 0.9, 0.3, 0.2, 0.4]
      thetaS = VS.convert thetaU
      trees  = map relabelParamsOrder $ filter (\t -> let v = VU.sum (compile dataset t thetaU) in not (isInfinite v || isNaN v)) trees'
      h = 1e-6
      gfd :: CompiledTree -> VS.Vector Double
      gfd ct = VS.generate (VS.length thetaS) $ \i ->
          let e = VS.fromList (map (\j -> if j == i then h else 0) [0 .. VS.length thetaS - 1])
              (fp, _) = evalGradVec ct (VS.zipWith (+) thetaS e)
              (fm, _) = evalGradVec ct (VS.zipWith (-) thetaS e)
          in (fp - fm) / (2 * h)
  forM_ (zip [0..] trees) $ \(i, t) -> do
      let ct = compileTree dataset y Nothing t
          cts = compileTreeMulti dataset y Nothing t
          (f1, g1) = evalGradVec ct thetaS
          (f0, g0) = evalGrad ct thetaS
          (f2, g2) = evalGradMulti cts thetaS
          fd = gfd ct
      putStrLn ("benchgrad tree " ++ show i ++ " obj=" ++ show f1)
      assertBool ("tree " ++ show i ++ " evalGradVec objective != evalGrad") (abs (f1 - f0) < 1e-6 * max 1 (abs f0))
      assertBool ("tree " ++ show i ++ " evalGradMulti objective != evalGrad") (abs (f2 - f0) < 1e-6 * max 1 (abs f0))
      assertBool ("tree " ++ show i ++ " evalGradVec gradient mismatch") (and (zipWith (\a b -> abs (a - b) < 1e-3 * max 1 (abs a)) (VS.toList g1) (VS.toList fd)))
      assertBool ("tree " ++ show i ++ " evalGrad gradient mismatch") (and (zipWith (\a b -> abs (a - b) < 1e-3 * max 1 (abs a)) (VS.toList g0) (VS.toList fd)))
      assertBool ("tree " ++ show i ++ " evalGradMulti gradient != evalGrad") (and (zipWith (\a b -> abs (a - b) < 1e-9 * max 1 (abs a)) (VS.toList g0) (VS.toList g2)))

main :: IO ()
main = do
  let t1 = TestLabel "compile" test_compile
      t2 = TestLabel "grad" test_grad

  counts <- runTestTT $ TestList
    [ t1
    , t2
    , TestLabel "benchgrad" test_benchgrad
    , TestLabel "eqsat" EqSatTests.tests
    , TestLabel "store" StoreTests.tests
    ]
  if failures counts /= 0 || errors counts /= 0
    then error "Some tests failed"
    else pure ()