haskell-fsrs-7.0.0: test/Test/FSRS/Golden.hs
-- | Every function of the model, checked against vectors produced by the
-- pure-Python transcription of the upstream 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 "difficulty" goldenDifficultyVectors checkDifficulty
, labelled "stability blocks" goldenHalfStabilityVectors checkHalfStability
, labelled "transition function" goldenTransitionVectors checkTransition
, 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)
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)
checkCurve :: CurveVector -> Assertion
checkCurve v =
close tol (show v) (cvRetrievability v) $
retrievability (paramsAt (cvParams v)) (cvElapsedDays v) (cvStability 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 rating
where
params = paramsAt (dvParams v)
rating = ratingAt (dvRating v)
checkHalfStability :: HalfStabilityVector -> Assertion
checkHalfStability v =
close tol (show v) (hsNextStability v) $
stabilityAfterReview
block
(MemoryState (hsStability v) (hsDifficulty v))
(hsRetrievability v)
(ratingAt (hsRating v))
where
params = paramsAt (hsParams v)
block = (if hsLongTerm v then longTermWeights else shortTermWeights) params
checkTransition :: TransitionVector -> Assertion
checkTransition v =
close tol (show v) (tvCoefficient v) $
transitionCoefficient (paramsAt (tvParams v)) (tvElapsedDays v)
checkStep :: StepVector -> Assertion
checkStep v = do
close tol (show v <> " [stability]") expectedS (memoryStability actual)
close tol (show v <> " [difficulty]") expectedD (memoryDifficulty actual)
where
(expectedS, expectedD) = svNextState v
actual =
nextMemoryState
(paramsAt (svParams v))
(uncurry MemoryState <$> svState v)
(svElapsedDays v)
(ratingAt (svRating v))
checkInterval :: IntervalVector -> Assertion
checkInterval v =
close intervalTol (show v) (ivInterval v) $
nextIntervalDays (paramsAt (ivParams v)) (ivDesiredRetention v) (ivStability v)
checkReplay :: ReplayVector -> Assertion
checkReplay v = case replayReviews params reviews of
Nothing -> assertFailure (show v <> ": replayReviews returned Nothing")
Just actual -> do
close tol (show v <> " [stability]") expectedS (memoryStability actual)
close tol (show v <> " [difficulty]") expectedD (memoryDifficulty actual)
where
params = paramsAt (rvParams v)
reviews = [(dt, ratingAt g) | (dt, g) <- rvReviews v]
(expectedS, expectedD) = rvFinalState v