daily learning queue

The way we were handling 1 day+ learning intervals was not great - they'd show
up at the start of a new day before normal reviews, meaning the hardest cards
came first. In previous Anki versions we deliberately sorted the queue in the
opposite order to prevent that. When relearning the cards the next day, if you
failed a card and expected to see it in 10 minutes that wouldn't happen
either, as all the overdue cards took precedence.

To fix this, we put cards that are due tomorrow or later into a separate queue
(queue 3), and pull cards from that queue only after the reviews are done. In
the future it might also be nice to move overdue learning cards into that
queue automatically at the start of a session.
This commit is contained in:
Damien Elmes 2012-05-20 16:56:14 +09:00
parent 32fde2a072
commit b6bdd4aa21
3 changed files with 112 additions and 16 deletions

View file

@ -585,7 +585,8 @@ where c.nid == f.id
self.db.execute("delete from revlog where id = ?", last) self.db.execute("delete from revlog where id = ?", last)
# and finally, update daily counts # and finally, update daily counts
# fixme: what to do in cramming case? # fixme: what to do in cramming case?
type = ("new", "lrn", "rev")[c.queue] n = 1 if c.queue == 3 else c.queue
type = ("new", "lrn", "rev")[n]
self.sched._updateStats(c, type, -1) self.sched._updateStats(c, type, -1)
return c.id return c.id

View file

@ -11,12 +11,9 @@ from anki.lang import _, ngettext
from anki.consts import * from anki.consts import *
from anki.hooks import runHook from anki.hooks import runHook
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram # revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
# queue types: 0=new/cram, 1=lrn, 2=rev, -1=suspended, -2=buried # positive revlog intervals are in days (rev), negative in seconds (lrn)
# positive intervals are in days (rev), negative intervals in seconds (lrn)
# fixme:
# - should log cram reps as cramming
class Scheduler(object): class Scheduler(object):
name = "std" name = "std"
@ -69,7 +66,7 @@ class Scheduler(object):
card.ivl = self._dynIvlBoost(card) card.ivl = self._dynIvlBoost(card)
card.odue = self.today + card.ivl card.odue = self.today + card.ivl
self._updateStats(card, 'new') self._updateStats(card, 'new')
if card.queue == 1: if card.queue in (1, 3):
self._answerLrnCard(card, ease) self._answerLrnCard(card, ease)
if not wasNew: if not wasNew:
self._updateStats(card, 'lrn') self._updateStats(card, 'lrn')
@ -272,6 +269,10 @@ order by due""" % self._deckLimit(),
return self._getNewCard() return self._getNewCard()
# card due for review? # card due for review?
c = self._getRevCard() c = self._getRevCard()
if c:
return c
# day learning card due?
c = self._getLrnDayCard()
if c: if c:
return c return c
# new cards left? # new cards left?
@ -383,20 +384,29 @@ select count() from
c = self.col.decks.confForDid(g['id']) c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1]) return max(0, c['new']['perDay'] - g['newToday'][1])
# Learning queue # Learning queues
########################################################################## ##########################################################################
def _resetLrnCount(self): def _resetLrnCount(self):
# sub-day
self.lrnCount = self.col.db.scalar(""" self.lrnCount = self.col.db.scalar("""
select sum(left/1000) from (select left from cards where select sum(left/1000) from (select left from cards where
did in %s and queue = 1 and due < ? limit %d)""" % ( did in %s and queue = 1 and due < ? limit %d)""" % (
self._deckLimit(), self.reportLimit), self._deckLimit(), self.reportLimit),
self.dayCutoff) or 0 self.dayCutoff) or 0
# day
self.lrnCount += self.col.db.scalar("""
select count() from cards where did in %s and queue = 3
and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit),
self.today)
def _resetLrn(self): def _resetLrn(self):
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue = [] self._lrnQueue = []
self._lrnDayQueue = []
self._lrnDids = self.col.decks.active()[:]
# sub-day learning
def _fillLrn(self): def _fillLrn(self):
if not self.lrnCount: if not self.lrnCount:
return False return False
@ -421,6 +431,36 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
self.lrnCount -= card.left/1000 self.lrnCount -= card.left/1000
return card return card
# daily learning
def _fillLrnDay(self):
if not self.lrnCount:
return False
if self._lrnDayQueue:
return True
while self._lrnDids:
did = self._lrnDids[0]
# fill the queue with the current did
self._lrnDayQueue = self.col.db.list("""
select id from cards where
did = ? and queue = 3 and due <= ? limit ?""",
did, self.today, self.queueLimit)
if self._lrnDayQueue:
# order
r = random.Random()
r.seed(self.today)
r.shuffle(self._lrnDayQueue)
# is the current did empty?
if len(self._lrnDayQueue) < self.queueLimit:
self._lrnDids.pop(0)
return True
# nothing left in the deck; move to next
self._lrnDids.pop(0)
def _getLrnDayCard(self):
if self._fillLrnDay():
self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop())
def _answerLrnCard(self, card, ease): def _answerLrnCard(self, card, ease):
# ease 1=no, 2=yes, 3=remove # ease 1=no, 2=yes, 3=remove
conf = self._lrnConf(card) conf = self._lrnConf(card)
@ -463,15 +503,23 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
# not collapsed; add some randomness # not collapsed; add some randomness
delay *= random.uniform(1, 1.25) delay *= random.uniform(1, 1.25)
card.due = int(time.time() + delay) card.due = int(time.time() + delay)
# due today?
if card.due < self.dayCutoff: if card.due < self.dayCutoff:
self.lrnCount += card.left/1000 self.lrnCount += card.left/1000
# if the queue is not empty and there's nothing else to do, make # if the queue is not empty and there's nothing else to do, make
# sure we don't put it at the head of the queue and end up showing # sure we don't put it at the head of the queue and end up showing
# it twice in a row # it twice in a row
if self._lrnQueue and not self.revCount and not self.newCount: card.queue = 1
smallestDue = self._lrnQueue[0][0] if self._lrnQueue and not self.revCount and not self.newCount:
card.due = max(card.due, smallestDue+1) smallestDue = self._lrnQueue[0][0]
heappush(self._lrnQueue, (card.due, card.id)) card.due = max(card.due, smallestDue+1)
heappush(self._lrnQueue, (card.due, card.id))
else:
# the card is due in one or more days, so we need to use the
# day learn queue
ahead = ((card.due - self.dayCutoff) / 86400) + 1
card.due = self.today + ahead
card.queue = 3
self._logLrn(card, ease, conf, leaving, type, lastLeft) self._logLrn(card, ease, conf, leaving, type, lastLeft)
def _delayForGrade(self, conf, left): def _delayForGrade(self, conf, left):
@ -1019,7 +1067,7 @@ your short-term review workload will become."""))
def nextIvl(self, card, ease): def nextIvl(self, card, ease):
"Return the next interval for CARD, in seconds." "Return the next interval for CARD, in seconds."
if card.queue in (0,1): if card.queue in (0,1,3):
return self._nextLrnIvl(card, ease) return self._nextLrnIvl(card, ease)
elif ease == 1: elif ease == 1:
# lapsed # lapsed

View file

@ -197,6 +197,53 @@ def test_learn_collapsed():
c = d.sched.getCard() c = d.sched.getCard()
assert not c.q().endswith("2") assert not c.q().endswith("2")
def test_learn_day():
d = getEmptyDeck()
# add a note
f = d.newNote()
f['Front'] = u"one"
f = d.addNote(f)
d.sched.reset()
c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880]
# pass it
d.sched.answerCard(c, 2)
# two reps to graduate, 1 more today
assert c.left%1000 == 3
assert c.left/1000 == 1
assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard()
ni = d.sched.nextIvl
assert ni(c, 2) == 86400
# answering it will place it in queue 3
d.sched.answerCard(c, 2)
assert c.due == d.sched.today+1
assert c.queue == 3
assert not d.sched.getCard()
# for testing, move it back a day
c.due -= 1
c.flush()
d.reset()
assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard()
# nextIvl should work
assert ni(c, 2) == 86400*2
# if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1)
assert c.queue == 1
d.undo()
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 2)
# simulate the passing of another two days
c.due -= 2
c.flush()
d.reset()
# the last pass should graduate it into a review card
assert ni(c, 2) == 86400
d.sched.answerCard(c, 2)
assert c.queue == c.type == 2
def test_reviews(): def test_reviews():
d = getEmptyDeck() d = getEmptyDeck()
# add a note # add a note