more scheduler updates

- reimplement reviewEarly and newEarly by replacing parts of the scheduler,
  instead of adding special conditions
- remove references to isDue and priority (1,2,3,4) which is not necessary
  anymore
- add option to switch between per-day scheduling and due now scheduling
- newCardsToday() -> newCardsDoneToday()
- don't decrement counts for suspended cards
- make sure to update type when suspending/unsuspending
- fix findCards()
- set hardInterval = 1-1.1 on upgrade, or the default per day scheduling doesn't
  make sense
This commit is contained in:
Damien Elmes 2010-10-18 18:01:19 +09:00
parent ad743d850d
commit be4dea39b1
5 changed files with 113 additions and 79 deletions

View file

@ -73,7 +73,6 @@ class Card(object):
self.id = genID() self.id = genID()
# new cards start as new & due # new cards start as new & due
self.type = 2 self.type = 2
self.isDue = True
self.timerStarted = False self.timerStarted = False
self.timerStopped = False self.timerStopped = False
self.modified = time.time() self.modified = time.time()
@ -226,7 +225,7 @@ from cards where id = :id""", id=id)
return True return True
def toDB(self, s): def toDB(self, s):
"Write card to DB. Note that isDue assumes card is not spaced." "Write card to DB."
if self.reps == 0: if self.reps == 0:
self.type = 2 self.type = 2
elif self.successive: elif self.successive:
@ -260,7 +259,7 @@ matureEase4=:matureEase4,
yesCount=:yesCount, yesCount=:yesCount,
noCount=:noCount, noCount=:noCount,
spaceUntil = :spaceUntil, spaceUntil = :spaceUntil,
isDue = :isDue, isDue = 0,
type = :type, type = :type,
combinedDue = max(:spaceUntil, :due), combinedDue = max(:spaceUntil, :due),
relativeDelay = 0, relativeDelay = 0,

View file

@ -143,14 +143,13 @@ class Deck(object):
self.sessionStartReps = 0 self.sessionStartReps = 0
self.sessionStartTime = 0 self.sessionStartTime = 0
self.lastSessionStart = 0 self.lastSessionStart = 0
self.newEarly = False
self.reviewEarly = False
self.updateCutoff() self.updateCutoff()
self.setupStandardScheduler() self.setupStandardScheduler()
# if most recent deck var not defined, make sure defaults are set # if most recent deck var not defined, make sure defaults are set
if not self.s.scalar("select 1 from deckVars where key = 'leechFails'"): if not self.s.scalar("select 1 from deckVars where key = 'perDay'"):
self.setVarDefault("suspendLeeches", True) self.setVarDefault("suspendLeeches", True)
self.setVarDefault("leechFails", 16) self.setVarDefault("leechFails", 16)
self.setVarDefault("perDay", True)
def modifiedSinceSave(self): def modifiedSinceSave(self):
return self.modified > self.lastLoaded return self.modified > self.lastLoaded
@ -166,6 +165,13 @@ class Deck(object):
self.rebuildRevCount = self._rebuildRevCount self.rebuildRevCount = self._rebuildRevCount
self.rebuildNewCount = self._rebuildNewCount self.rebuildNewCount = self._rebuildNewCount
self.requeueCard = self._requeueCard self.requeueCard = self._requeueCard
self.timeForNewCard = self._timeForNewCard
self.updateNewCountToday = self._updateNewCountToday
self.finishScheduler = None
def _noop(self):
"Do nothing."
pass
def fillQueues(self): def fillQueues(self):
self.fillFailedQueue() self.fillFailedQueue()
@ -198,10 +204,10 @@ class Deck(object):
"and combinedDue < :lim", lim=self.dueCutoff) "and combinedDue < :lim", lim=self.dueCutoff)
self.updateNewCountToday() self.updateNewCountToday()
def updateNewCountToday(self): def _updateNewCountToday(self):
self.newCountToday = max(min( self.newCountToday = max(min(
self.newCount, self.newCardsPerDay - self.newCount, self.newCardsPerDay -
self.newCardsToday()), 0) self.newCardsDoneToday()), 0)
def _fillFailedQueue(self): def _fillFailedQueue(self):
if self.failedSoonCount and not self.failedQueue: if self.failedSoonCount and not self.failedQueue:
@ -295,8 +301,11 @@ end)""" + where)
"update cards set type = type + 3 where priority <= 0") "update cards set type = type + 3 where priority <= 0")
def updateCutoff(self): def updateCutoff(self):
if self.getBool("perDay"):
today = genToday(self) + datetime.timedelta(days=1) today = genToday(self) + datetime.timedelta(days=1)
self.dueCutoff = time.mktime(today.timetuple()) self.dueCutoff = time.mktime(today.timetuple())
else:
self.dueCutoff = time.time()
def reset(self): def reset(self):
# setup global/daily stats # setup global/daily stats
@ -329,15 +338,55 @@ end)""" + where)
if genToday(self) != self._dailyStats.day: if genToday(self) != self._dailyStats.day:
self._dailyStats = dailyStats(self) self._dailyStats = dailyStats(self)
# Review early
##########################################################################
def setupReviewEarlyScheduler(self):
self.fillRevQueue = self._fillRevEarlyQueue
self.rebuildRevCount = self._rebuildRevEarlyCount
self.finishScheduler = self._onReviewEarlyFinished
def resetAfterReviewEarly(self): def resetAfterReviewEarly(self):
ids = self.s.column0("select id from cards where priority = -1") ids = self.s.column0("select id from cards where priority = -1")
if ids: if ids:
self.updatePriorities(ids) self.updatePriorities(ids)
self.flushMod() self.flushMod()
if self.reviewEarly or self.newEarly:
self.reviewEarly = False def _onReviewEarlyFinished(self):
self.newEarly = False # clean up buried cards
self.checkDue() self.resetAfterReviewEarly()
# and go back to regular scheduler
self.setupStandardScheduler()
def _rebuildRevEarlyCount(self):
# in the future it would be nice to skip the first x days of due cards
extraLim = ""
self.revCount = self.s.scalar("""
select count() from cards where type = 1 and combinedDue > :now
%s""" % extraLim, now=self.dueCutoff)
def _fillRevEarlyQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.s.all("""
select id, factId from cards where type = 1 and combinedDue > :lim
order by combinedDue limit 50""", lim=self.dueCutoff)
self.revQueue.reverse()
# Learn more
##########################################################################
def setupLearnMoreScheduler(self):
self.rebuildNewCount = self._rebuildLearnMoreCount
self.updateNewCountToday = self._updateLearnMoreCountToday
self.finishScheduler = self.setupStandardScheduler
def _rebuildLearnMoreCount(self):
self.newCount = self.s.scalar("""
select count() from cards where type = 2 and combinedDue < :now
""", now=self.dueCutoff)
def _updateLearnMoreCountToday(self):
self.newCountToday = self.newCount
# Getting the next card # Getting the next card
########################################################################## ##########################################################################
@ -348,7 +397,7 @@ end)""" + where)
if id: if id:
return self.cardFromId(id, orm) return self.cardFromId(id, orm)
def getCardId(self): def getCardId(self, check=True):
"Return the next due card id, or None." "Return the next due card id, or None."
self.checkDailyStats() self.checkDailyStats()
self.fillQueues() self.fillQueues()
@ -368,34 +417,28 @@ end)""" + where)
if self.revNoSpaced(): if self.revNoSpaced():
return self.revQueue[-1][0] return self.revQueue[-1][0]
# new cards left? # new cards left?
if self.newQueue: if self.newCountToday:
return self.newQueue[-1][0] return self.newQueue[-1][0]
# # review ahead?
# if self.reviewEarly:
# id = self.getCardIdAhead()
# if id:
# return id
# else:
# self.resetAfterReviewEarly()
# self.checkDue()
# display failed cards early/last # display failed cards early/last
if self.showFailedLast() and self.failedQueue: if self.showFailedLast() and self.failedQueue:
return self.failedQueue[-1][0] return self.failedQueue[-1][0]
if check:
def getCardIdAhead(self): # check for expired cards, or new day rollover
"Return the first card that would become due." self.updateCutoff()
id = self.s.scalar(""" return self.getCardId(check=False)
select id from cards # if we're in a custom scheduler, we may need to switch back
where type = 1 and isDue = 0 and priority in (1,2,3,4) if self.finishScheduler:
order by combinedDue self.finishScheduler()
limit 1""") self.reset()
return id return self.getCardId()
# Get card: helper functions # Get card: helper functions
########################################################################## ##########################################################################
def timeForNewCard(self): def _timeForNewCard(self):
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
if not self.newCountToday:
return False
if self.newCardSpacing == NEW_CARDS_LAST: if self.newCardSpacing == NEW_CARDS_LAST:
return False return False
if self.newCardSpacing == NEW_CARDS_FIRST: if self.newCardSpacing == NEW_CARDS_FIRST:
@ -406,7 +449,7 @@ limit 1""")
"select 1 from cards where id = :id and priority = 4", "select 1 from cards where id = :id and priority = 4",
id = self.revQueue[-1][0]): id = self.revQueue[-1][0]):
return False return False
if self.newCardModulus and (self.newCountToday or self.newEarly): if self.newCardModulus:
return self._dailyStats.reps % self.newCardModulus == 0 return self._dailyStats.reps % self.newCardModulus == 0
else: else:
return False return False
@ -515,7 +558,6 @@ where factId in (select factId from %s limit 60))""" % (new, new))
# only update if card was not new # only update if card was not new
card.lastDue = card.due card.lastDue = card.due
card.due = self.nextDue(card, ease, oldState) card.due = self.nextDue(card, ease, oldState)
card.isDue = 0
card.lastFactor = card.factor card.lastFactor = card.factor
if lastDelay >= 0: if lastDelay >= 0:
# don't update factor if learning ahead # don't update factor if learning ahead
@ -536,22 +578,16 @@ where factId = :fid and id != :id""", fid=card.factId, id=card.id) or 0
space += time.time() space += time.time()
card.combinedDue = max(card.due, space) card.combinedDue = max(card.due, space)
# check what other cards we've spaced # check what other cards we've spaced
if self.reviewEarly:
extra = ""
else:
# if not reviewing early, make sure the current card is counted
# even if it was not due yet (it's a failed card)
extra = "or id = :cid"
for (type, count) in self.s.all(""" for (type, count) in self.s.all("""
select type, count(type) from cards select type, count(type) from cards
where factId = :fid and where factId = :fid and
(isDue = 1 %s) (combinedDue < :now or id = :cid)
group by type""" % extra, fid=card.factId, cid=card.id): group by type""", fid=card.factId, cid=card.id, now=self.dueCutoff):
if type == 0: if type == 0:
self.failedSoonCount -= count self.failedSoonCount -= count
elif type == 1: elif type == 1:
self.revCount -= count self.revCount -= count
else: elif type == 2:
self.newCount -= count self.newCount -= count
# bump failed count if necessary # bump failed count if necessary
if ease == 1: if ease == 1:
@ -561,15 +597,14 @@ group by type""" % extra, fid=card.factId, cid=card.id):
update cards set update cards set
spaceUntil = :space, spaceUntil = :space,
combinedDue = max(:space, due), combinedDue = max(:space, due),
modified = :now, modified = :now
isDue = 0
where id != :id and factId = :factId""", where id != :id and factId = :factId""",
id=card.id, space=space, now=now, factId=card.factId) id=card.id, space=space, now=now, factId=card.factId)
card.spaceUntil = 0 card.spaceUntil = 0
self.spacedFacts[card.factId] = space self.spacedFacts[card.factId] = space
# temp suspend if learning ahead # temp suspend if it's a review card & we're reviewing early
if self.reviewEarly and lastDelay < 0: if oldSuc and lastDelay < 0:
if oldSuc or lastDelaySecs > self.delay0 or not self._showFailedLast(): if lastDelaySecs > self.delay0:
card.priority = -1 card.priority = -1
# card stats # card stats
anki.cards.Card.updateStats(card, ease, oldState) anki.cards.Card.updateStats(card, ease, oldState)
@ -715,7 +750,7 @@ factor = 2.5, reps = 0, successive = 0, averageTime = 0, reviewTime = 0,
youngEase0 = 0, youngEase1 = 0, youngEase2 = 0, youngEase3 = 0, youngEase0 = 0, youngEase1 = 0, youngEase2 = 0, youngEase3 = 0,
youngEase4 = 0, matureEase0 = 0, matureEase1 = 0, matureEase2 = 0, youngEase4 = 0, matureEase0 = 0, matureEase1 = 0, matureEase2 = 0,
matureEase3 = 0,matureEase4 = 0, yesCount = 0, noCount = 0, matureEase3 = 0,matureEase4 = 0, yesCount = 0, noCount = 0,
spaceUntil = 0, isDue = 0, type = 2, spaceUntil = 0, type = 2,
combinedDue = created, modified = :now, due = created combinedDue = created, modified = :now, due = created
where id in %s""" % ids2str(ids), now=time.time(), new=0) where id in %s""" % ids2str(ids), now=time.time(), new=0)
if self.newCardOrder == NEW_CARDS_RANDOM: if self.newCardOrder == NEW_CARDS_RANDOM:
@ -772,8 +807,7 @@ reps = 1,
successive = 1, successive = 1,
yesCount = 1, yesCount = 1,
firstAnswered = :t, firstAnswered = :t,
type = 1, type = 1
isDue = 0
where id = :id""", vals) where id = :id""", vals)
self.flushMod() self.flushMod()
@ -783,9 +817,8 @@ where id = :id""", vals)
def nextDueMsg(self): def nextDueMsg(self):
next = self.earliestTime() next = self.earliestTime()
if next: if next:
newCount = self.s.scalar(""" newCount = self.s.scalar(
select count() from cards where type = 2 "select count() from cards where type = 2")
and priority in (1,2,3,4)""")
newCardsTomorrow = min(newCount, self.newCardsPerDay) newCardsTomorrow = min(newCount, self.newCardsPerDay)
cards = self.cardsDueBy(time.time() + 86400) cards = self.cardsDueBy(time.time() + 86400)
msg = _('''\ msg = _('''\
@ -812,8 +845,8 @@ This may be in the past if the deck is not finished.
If the deck has no (enabled) cards, return None. If the deck has no (enabled) cards, return None.
Ignore new cards.""" Ignore new cards."""
return self.s.scalar(""" return self.s.scalar("""
select combinedDue from cards where priority in (1,2,3,4) and select combinedDue from cards where type in (0, 1)
type in (0, 1) order by combinedDue limit 1""") order by combinedDue limit 1""")
def earliestTimeStr(self, next=None): def earliestTimeStr(self, next=None):
"""Return the relative time to the earliest card as a string.""" """Return the relative time to the earliest card as a string."""
@ -828,10 +861,9 @@ type in (0, 1) order by combinedDue limit 1""")
"Number of cards due at TIME. Ignore new cards" "Number of cards due at TIME. Ignore new cards"
return self.s.scalar(""" return self.s.scalar("""
select count(id) from cards where combinedDue < :time select count(id) from cards where combinedDue < :time
and priority in (1,2,3,4) and type in (0, 1)""", time=time) and type in (0, 1)""", time=time)
def deckFinishedMsg(self): def deckFinishedMsg(self):
self.resetAfterReviewEarly()
spaceSusp = "" spaceSusp = ""
c= self.spacedCardCount() c= self.spacedCardCount()
if c: if c:
@ -944,9 +976,12 @@ group by cardTags.cardId""" % limit)
"update cards set priority = :pri %s where id in %s " "update cards set priority = :pri %s where id in %s "
"and priority != :pri and priority >= -2") % ( "and priority != :pri and priority >= -2") % (
extra, ids2str(cs)), pri=pri, m=time.time()) extra, ids2str(cs)), pri=pri, m=time.time())
cnt = self.s.execute( self.s.execute(
"update cards set isDue = 0 where type in (0,1,2) and " "update cards set type = type + 3 where type in (0,1,2) and "
"priority = 0 and isDue = 1").rowcount "priority = 0").rowcount
self.s.execute(
"update cards set type = type - 3 where type in (3,4,5) and "
"priority > 0").rowcount
self.reset() self.reset()
def updatePriority(self, card): def updatePriority(self, card):
@ -960,16 +995,17 @@ group by cardTags.cardId""" % limit)
def suspendCards(self, ids): def suspendCards(self, ids):
self.startProgress() self.startProgress()
self.s.statement( self.s.statement(
"update cards set isDue=0, priority=-3, modified=:t " "update cards set type = type + 3, priority=-3, modified=:t "
"where id in %s" % ids2str(ids), t=time.time()) "where type in (0,1,2) and id in %s" % ids2str(ids), t=time.time())
self.flushMod() self.flushMod()
self.reset() self.reset()
self.finishProgress() self.finishProgress()
def unsuspendCards(self, ids): def unsuspendCards(self, ids):
self.startProgress() self.startProgress()
self.s.statement( self.s.statement("""
"update cards set priority=0, modified=:t where id in %s" % update cards set type = type - 3, priority=0, modified=:t
where type in (3,4,5) and id in %s""" %
ids2str(ids), t=time.time()) ids2str(ids), t=time.time())
self.updatePriorities(ids) self.updatePriorities(ids)
self.flushMod() self.flushMod()
@ -997,7 +1033,7 @@ select count(id) from cards where priority = 0""")
# Counts related to due cards # Counts related to due cards
########################################################################## ##########################################################################
def newCardsToday(self): def newCardsDoneToday(self):
return (self._dailyStats.newEase0 + return (self._dailyStats.newEase0 +
self._dailyStats.newEase1 + self._dailyStats.newEase1 +
self._dailyStats.newEase2 + self._dailyStats.newEase2 +
@ -1008,7 +1044,7 @@ select count(id) from cards where priority = 0""")
"Number of spaced new cards." "Number of spaced new cards."
return self.s.scalar(""" return self.s.scalar("""
select count(cards.id) from cards where select count(cards.id) from cards where
type = 2 and isDue = 0 and priority in (1,2,3,4) and combinedDue > :now type = 2 and combinedDue > :now
and due < :now""", now=time.time()) and due < :now""", now=time.time())
def isEmpty(self): def isEmpty(self):
@ -1993,8 +2029,9 @@ cardTags.tagId in %s""" % ids2str(ids)
qquery += "select id from cards where type = %d" % n qquery += "select id from cards where type = %d" % n
elif token == "delayed": elif token == "delayed":
qquery += ("select id from cards where " qquery += ("select id from cards where "
"due < %d and isDue = 0 and " "due < %d and combinedDue > %d and "
"priority in (1,2,3,4)") % time.time() "type in (0,1,2)") % (
self.dueCutoff, self.dueCutoff)
elif token == "suspended": elif token == "suspended":
qquery += ("select id from cards where " qquery += ("select id from cards where "
"priority = -3") "priority = -3")
@ -2003,7 +2040,7 @@ cardTags.tagId in %s""" % ids2str(ids)
"priority = 0") "priority = 0")
else: # due else: # due
qquery += ("select id from cards where " qquery += ("select id from cards where "
"type in (0,1) and isDue = 1") "type in (0,1) and combinedDue < %d") % self.dueCutoff
elif type == SEARCH_FID: elif type == SEARCH_FID:
if fidquery: if fidquery:
if isNeg: if isNeg:
@ -2476,9 +2513,7 @@ select id from fields where factId not in (select id from facts)""")
problems.append(_("Deleted: ") + "%s %s" % tuple(card)) problems.append(_("Deleted: ") + "%s %s" % tuple(card))
self.s.flush() self.s.flush()
if not quick: if not quick:
# fix problems with cards being scheduled when not due
self.updateProgress() self.updateProgress()
self.s.statement("update cards set isDue = 0")
# these sometimes end up null on upgrade # these sometimes end up null on upgrade
self.s.statement("update models set source = 0 where source is null") self.s.statement("update models set source = 0 where source is null")
self.s.statement( self.s.statement(
@ -3399,6 +3434,9 @@ nextFactor, reps, thinkingTime, yesCount, noCount from reviewHistory""")
DeckStorage._addIndices(deck) DeckStorage._addIndices(deck)
# new type handling # new type handling
deck.rebuildTypes() deck.rebuildTypes()
# per-day scheduling necessitates an increase here
deck.hardIntervalMin = 1
deck.hardIntervalMax = 1.1
deck.version = 44 deck.version = 44
deck.s.commit() deck.s.commit()
# executing a pragma here is very slow on large decks, so we store # executing a pragma here is very slow on large decks, so we store

View file

@ -128,7 +128,6 @@ matureEase4 = 0,
yesCount = 0, yesCount = 0,
noCount = 0, noCount = 0,
spaceUntil = 0, spaceUntil = 0,
isDue = 1,
type = 2, type = 2,
combinedDue = created, combinedDue = created,
modified = :now modified = :now

View file

@ -79,11 +79,10 @@ class DeckGraphs(object):
t = time.time() t = time.time()
young = self.deck.s.all(""" young = self.deck.s.all("""
select interval, combinedDue select interval, combinedDue
from cards where priority in (1,2,3,4) and from cards where type in (0, 1) and interval <= 21""")
type in (0, 1) and interval <= 21""")
mature = self.deck.s.all(""" mature = self.deck.s.all("""
select interval, combinedDue select interval, combinedDue
from cards where type = 1 and priority in (1,2,3,4) and interval > 21""") from cards where type = 1 and interval > 21""")
for (src, dest) in [(young, daysYoung), for (src, dest) in [(young, daysYoung),
(mature, daysMature)]: (mature, daysMature)]:

View file

@ -212,7 +212,6 @@ where factId in (%s)""" % ",".join([str(s) for s in factIds]))
data['tags'] = u"" data['tags'] = u""
self.cardIds.append(data['id']) self.cardIds.append(data['id'])
data['combinedDue'] = data['due'] data['combinedDue'] = data['due']
data['isDue'] = data['combinedDue'] < time.time()
return data return data
def stripInvalid(self, cards): def stripInvalid(self, cards):