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 |