packages feed

haskell-fsrs-7.1.0: test/Test/FSRS/Golden.hs

-- | Every function of the model, checked against vectors produced by the
-- pure-Python transcription of the reference implementation.
--
-- The two implementations perform the same floating-point operations in the
-- same order on the same libm, so agreement is expected to the last few bits;
-- the tolerances below are deliberately much tighter than "close enough".
module Test.FSRS.Golden (tests) where

import Control.Monad (unless)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertFailure, testCase, (@?=))

import FSRS
import Test.FSRS.Gen (approxEqual, relativeError)
import Test.FSRS.GoldenData

-- | Tolerance for everything but the root-finder: bit-for-bit agreement with a
-- little slack for the last ulp.
tol :: Double
tol = 1.0e-12

-- | The interval solver converges by a different route than the Python
-- bisection used to produce the vectors, so it only agrees to about a
-- nanosecond in relative terms.
intervalTol :: Double
intervalTol = 1.0e-9

tests :: TestTree
tests =
  testGroup
    "golden vectors"
    [ testCase "the golden parameter sets are all valid" $
        mapM_
          (\ws -> validateParameters ws @?= [])
          goldenParameterSets
    , testCase "the first golden parameter set is the default one" $
        take 1 goldenParameterSets @?= [parametersToList defaultParameters]
    , labelled "forgetting curve" goldenCurveVectors checkCurve
    , labelled "fast-trace recall" goldenFastRecallVectors checkFastRecall
    , labelled "difficulty" goldenDifficultyVectors checkDifficulty
    , labelled "stability blocks" goldenHalfStabilityVectors checkHalfStability
    , labelled "memory-state transition" goldenStepVectors checkStep
    , labelled "interval inversion" goldenIntervalVectors checkInterval
    , labelled "replayed review histories" goldenReplayVectors checkReplay
    ]

labelled :: String -> [a] -> (a -> Assertion) -> TestTree
labelled name vectors check =
  testCase (name <> " (" <> show (length vectors) <> " vectors)") $
    mapM_ check vectors

paramsAt :: Int -> Parameters
paramsAt i = case mkParameters (goldenParameterSets !! i) of
  Right p -> p
  Left errs -> error ("golden parameter set " <> show i <> " is invalid: " <> show errs)

ratingAt :: Int -> Rating
ratingAt n = case ratingFromInt n of
  Just r -> r
  Nothing -> error ("golden vector has a bad rating: " <> show n)

-- | The vectors spell states out as @(S, D, S_fast)@ triples.
stateOf :: (Double, Double, Double) -> MemoryState
stateOf (s, d, sFast) = MemoryState s d sFast

close :: Double -> String -> Double -> Double -> Assertion
close t label expected actual =
  unless (approxEqual t t actual expected) $
    assertFailure $
      label
        <> "\n  expected: "
        <> show expected
        <> "\n  actual:   "
        <> show actual
        <> "\n  relative error: "
        <> show (relativeError actual expected)

closeState :: Double -> String -> (Double, Double, Double) -> MemoryState -> Assertion
closeState t label (s, d, sFast) actual = do
  close t (label <> " [stability]") s (memoryStability actual)
  close t (label <> " [difficulty]") d (memoryDifficulty actual)
  close t (label <> " [fast stability]") sFast (memoryStabilityFast actual)

checkCurve :: CurveVector -> Assertion
checkCurve v =
  close tol (show v) (cvRetrievability v) $
    retrievability (paramsAt (cvParams v)) (cvElapsedDays v) (stateOf (cvState v))

checkFastRecall :: FastRecallVector -> Assertion
checkFastRecall v =
  close tol (show v) (frRecall v) $
    fastTraceRetrievability (paramsAt (frParams v)) (frElapsedDays v) (frStabilityFast v)

checkDifficulty :: DifficultyVector -> Assertion
checkDifficulty v =
  close tol (show v) (dvNextDifficulty v) $
    case dvDifficulty v of
      Nothing -> initialDifficulty params rating
      Just d -> nextDifficulty params d (dvRetrievability v) rating
  where
    params = paramsAt (dvParams v)
    rating = ratingAt (dvRating v)

checkHalfStability :: HalfStabilityVector -> Assertion
checkHalfStability v =
  close tol (show v) (hsNextStability v) $
    stabilityAfterReview
      block
      (hsStability v)
      (hsDifficulty v)
      (hsRetrievability v)
      (ratingAt (hsRating v))
  where
    params = paramsAt (hsParams v)
    block = (if hsSlowTrace v then slowTraceWeights else fastTraceWeights) params

checkStep :: StepVector -> Assertion
checkStep v = closeState tol (show v) (svNextState v) $
  nextMemoryState
    (paramsAt (svParams v))
    (stateOf <$> svState v)
    (svElapsedDays v)
    (ratingAt (svRating v))

checkInterval :: IntervalVector -> Assertion
checkInterval v =
  close intervalTol (show v) (ivInterval v) $
    nextIntervalDays (paramsAt (ivParams v)) (ivDesiredRetention v) (stateOf (ivState v))

checkReplay :: ReplayVector -> Assertion
checkReplay v = case replayReviews params reviews of
  Nothing -> assertFailure (show v <> ": replayReviews returned Nothing")
  Just actual -> closeState tol (show v) (rvFinalState v) actual
  where
    params = paramsAt (rvParams v)
    reviews = [(dt, ratingAt g) | (dt, g) <- rvReviews v]