Greatly improve scheduling algorithm
John Smith

John Smith commited on 2022-206 08:31:53
Showing 2 changed files, with 19 additions and 8 deletions.


The scheduling algorithm is now similar to the SM-2-based algorithm
used in Anki, with various simplifications, such as the omission of
a separated "learning" phase.

This change required adding an `Ease` field to each prompt.
Memoire remains backwards-compatible by automatically adding
the default ease to any cards which have no such field.
... ...
@@ -2,7 +2,7 @@
2 2
     a prompt consists of prompt text (with an answer-insertion point),
3 3
     a correct answer, the review timestamp, the review delay,
4 4
     counts of successful and failed reviews,
5
-    and a creation timestamp
5
+    a creation timestamp, and an ease measurement
6 6
 
7 7
     timestamps are seconds from the Unix epoch
8 8
     review delay is in seconds
... ...
@@ -14,21 +14,22 @@ Prompt.__index = Prompt
14 14
 --[[
15 15
     the answer-insertion point is indicated with a "%A" in the prompt text
16 16
 ]]
17
-function Prompt:new(pt, ca, rts, rd, sc, fc, ct)
17
+function Prompt:new(pt, ca, rts, rd, sc, fc, ct, ez)
18 18
     local o = {}
19 19
     setmetatable(o, self)
20 20
     o.text = pt
21 21
     o.ans = ca
22 22
     o.time = tonumber(rts)
23 23
     o.delay = tonumber(rd)
24
-    o.succ = sc
25
-    o.fail = fc
24
+    o.succ = tonumber(sc)
25
+    o.fail = tonumber(fc)
26 26
     o.created = ct
27
+    o.ease = tonumber(ez)
27 28
     return o
28 29
 end
29 30
 
30 31
 function Prompt:__tostring()
31
-    return string.format("\"%s\" (\"%s\") %d+%d %d/%d", self.text, self.ans, self.time, self.delay, self.succ, self.fail)
32
+    return string.format("\"%s\" (\"%s\") %d -> %d+%d*%d %d/%d", self.text, self.ans, self.created, self.time, self.delay, self.ease, self.succ, self.fail)
32 33
 end
33 34
 
34 35
 --[[
... ...
@@ -60,7 +61,8 @@ function Prompt:fromrecs(recs)
60 61
                     (rec.LastDelay or {})[ctr],
61 62
                     (rec.Successes or {})[ctr],
62 63
                     (rec.Failures or {})[ctr],
63
-                    (rec.Created or {})[1]
64
+                    (rec.Created or {})[1],
65
+                    (rec.Ease or {})[ctr] or "2.5"
64 66
                 ))
65 67
                 m, n = opt:find("%{[^%}]+%}", n)
66 68
                 ctr = ctr + 1
... ...
@@ -80,7 +82,7 @@ function Prompt:torec(prompts)
80 82
         return {}
81 83
     end
82 84
     local fspt = prompts[1]:spt()
83
-    local ret = { PromptText = { fspt }, LastReview = {}, LastDelay = {}, Successes = {}, Failures = {} }
85
+    local ret = { PromptText = { fspt }, LastReview = {}, LastDelay = {}, Successes = {}, Failures = {}, Ease = {} }
84 86
     if prompts[1].created then
85 87
         ret.Created = { prompts[1].created }
86 88
     end
... ...
@@ -93,6 +95,7 @@ function Prompt:torec(prompts)
93 95
         ret.LastDelay[i] = pr.delay
94 96
         ret.Successes[i] = pr.succ
95 97
         ret.Failures[i] = pr.fail
98
+        ret.Ease[i] = pr.ease
96 99
     end
97 100
     return ret
98 101
 end
... ...
@@ -10,7 +10,14 @@
10 10
 local function schedule(prompt, revtime, revsucc, conf)
11 11
     if revsucc and revsucc > 0 then
12 12
         local ad = revtime - (prompt.time or revtime)
13
-        return (conf:get("DelayRatio") or 2.5) ^ revsucc * (ad + math.max(40000 - 0.5 * ad, 0))
13
+        local adm = ad + math.max(40000 - 0.5 * ad, 0)
14
+        -- SM2, as explained by Bjornstad
15
+        -- https://controlaltbackspace.org/memory/spaced-repetition-from-the-ground-up/
16
+        if revsucc < 0.8 then
17
+            return (conf:get("HardRatio") or 1.2) * revsucc * 2 * adm
18
+        else
19
+            return prompt.ease * (1 + 2 * ((conf:get("EasyRatio") or 1.3) - 1) * (revsucc - 1)) * adm
20
+        end
14 21
     else
15 22
         return 15
16 23
     end
... ...
@@ -29,6 +36,7 @@ local function update(prompt, revtime, revsucc, conf)
29 36
     -- allowed random variation, as a fraction of the "optimal" duration
30 37
     local ARV = conf:get("AllowedRandomVariation") or 0.1
31 38
     prompt.delay = schedule(prompt, revtime, revsucc, conf) * ((1 - ARV) + 2 * ARV * math.random())
39
+    prompt.ease = prompt.ease + ({ -0.2, -0.15, 0, 0.15 })[2 * revsucc + 1]
32 40
     prompt.time = revtime
33 41
 end
34 42
 
35 43