generalize into 'dynamic decks'

- search and limits are embedded in the deck
- decks can be refreshed
- they have the option to treat due reviews normally rather than cram them
- some options are inherited from the original deck, others taken from the
  dynamic deck
This commit is contained in:
Damien Elmes 2012-03-10 19:41:37 +09:00
parent f6b2e69669
commit da07e15a87
4 changed files with 140 additions and 76 deletions

View file

@ -128,9 +128,6 @@ lapses=?, left=?, odue=?, did=? where id = ?""",
def model(self): def model(self):
return self.col.models.get(self.note().mid) return self.col.models.get(self.note().mid)
def deckConf(self):
return self.col.decks.confForDid(self.did)
def template(self): def template(self):
return self.model()['tmpls'][self.ord] return self.model()['tmpls'][self.ord]
@ -140,4 +137,5 @@ lapses=?, left=?, odue=?, did=? where id = ?""",
def timeTaken(self): def timeTaken(self):
"Time taken to answer card, in integer MS." "Time taken to answer card, in integer MS."
total = int((time.time() - self.timerStarted)*1000) total = int((time.time() - self.timerStarted)*1000)
return min(total, self.deckConf()['maxTaken']*1000) conf = self.col.decks.confForDid(self.odid or self.did)
return min(total, conf['maxTaken']*1000)

View file

@ -19,6 +19,24 @@ defaultDeck = {
'conf': 1, 'conf': 1,
'usn': 0, 'usn': 0,
'desc': "", 'desc': "",
'dyn': 0,
}
defaultDynamicDeck = {
'newToday': [0, 0],
'revToday': [0, 0],
'lrnToday': [0, 0],
'timeToday': [0, 0],
'dyn': 1,
'desc': "",
'usn': 0,
'delays': [1, 10],
'separate': True,
'fmult': 0,
'cramRev': True,
'search': "",
'limit': 100,
'order': 'oldestSeen',
} }
defaultConf = { defaultConf = {
@ -39,13 +57,6 @@ defaultConf = {
# type 0=suspend, 1=tagonly # type 0=suspend, 1=tagonly
'leechAction': 0, 'leechAction': 0,
}, },
'cram': {
'delays': [1, 5, 10],
'resched': True,
'reset': True,
'mult': 0,
'minInt': 1,
},
'rev': { 'rev': {
'perDay': 100, 'perDay': 100,
'ease4': 1.3, 'ease4': 1.3,
@ -91,7 +102,7 @@ class DeckManager(object):
# Deck save/load # Deck save/load
############################################################# #############################################################
def id(self, name, create=True): def id(self, name, create=True, type=defaultDeck):
"Add a deck with NAME. Reuse deck if already exists. Return id as int." "Add a deck with NAME. Reuse deck if already exists. Return id as int."
name = name.replace("'", "").replace('"', '') name = name.replace("'", "").replace('"', '')
for id, g in self.decks.items(): for id, g in self.decks.items():
@ -99,7 +110,7 @@ class DeckManager(object):
return int(id) return int(id)
if not create: if not create:
return None return None
g = copy.deepcopy(defaultDeck) g = copy.deepcopy(type)
if "::" in name: if "::" in name:
# not top level; ensure all parents exist # not top level; ensure all parents exist
self._ensureParents(name) self._ensureParents(name)
@ -123,10 +134,11 @@ class DeckManager(object):
if not str(did) in self.decks: if not str(did) in self.decks:
return return
deck = self.get(did) deck = self.get(did)
if deck.get('cram'): print "fixme: add dyn to old decks"
if deck['dyn']:
# deleting a cramming deck returns cards to their previous deck # deleting a cramming deck returns cards to their previous deck
# rather than deleting the cards # rather than deleting the cards
self.col.sched.remCram(did) self.col.sched.remDyn(did)
else: else:
# delete children first # delete children first
if childrenToo: if childrenToo:
@ -233,7 +245,13 @@ class DeckManager(object):
return self.dconf.values() return self.dconf.values()
def confForDid(self, did): def confForDid(self, did):
return self.getConf(self.get(did)['conf']) deck = self.get(did)
if 'conf' in deck:
conf = self.getConf(deck['conf'])
conf['dyn'] = False
return conf
# dynamic decks have embedded conf
return deck
def getConf(self, confId): def getConf(self, confId):
return self.dconf[str(confId)] return self.dconf[str(confId)]
@ -370,3 +388,10 @@ class DeckManager(object):
for c in self.allConf(): for c in self.allConf():
c['usn'] = 0 c['usn'] = 0
self.save() self.save()
# Dynamic decks
##########################################################################
def newDyn(self, name):
"Return a new dynamic deck and set it as the current deck."
return self.id(name, type=defaultDynamicDeck)

View file

@ -28,7 +28,6 @@ class Scheduler(object):
self.reportLimit = 1000 self.reportLimit = 1000
# fixme: replace reps with deck based counts # fixme: replace reps with deck based counts
self.reps = 0 self.reps = 0
self._cramming = False
self._updateCutoff() self._updateCutoff()
def getCard(self): def getCard(self):
@ -41,7 +40,6 @@ class Scheduler(object):
def reset(self): def reset(self):
deck = self.col.decks.current() deck = self.col.decks.current()
self._cramming = deck.get('cram')
self._updateCutoff() self._updateCutoff()
self._resetLrn() self._resetLrn()
self._resetRev() self._resetRev()
@ -61,10 +59,10 @@ class Scheduler(object):
card.type = 1 card.type = 1
# init reps to graduation # init reps to graduation
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
# cramming? # dynamic?
if self._cramming and card.type == 2: if card.odid and card.type == 2:
# reviews get their ivl boosted on first sight # reviews get their ivl boosted on first sight
card.ivl = self._cramIvlBoost(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 == 1:
@ -306,7 +304,7 @@ select id, due from cards where did = ? and queue = 0 limit ?""", did, lim)
(id, due) = self._newQueue.pop() (id, due) = self._newQueue.pop()
# move any siblings to the end? # move any siblings to the end?
conf = self.col.decks.confForDid(self._newDids[0]) conf = self.col.decks.confForDid(self._newDids[0])
if conf['new']['separate']: if conf['dyn'] or conf['new']['separate']:
n = len(self._newQueue) n = len(self._newQueue)
while self._newQueue and self._newQueue[-1][1] == due: while self._newQueue and self._newQueue[-1][1] == due:
self._newQueue.insert(0, self._newQueue.pop()) self._newQueue.insert(0, self._newQueue.pop())
@ -332,8 +330,6 @@ select id, due from cards where did = ? and queue = 0 limit ?""", did, lim)
"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.newCount: if not self.newCount:
return False return False
if self._cramming:
return True
if self.col.conf['newSpread'] == NEW_CARDS_LAST: if self.col.conf['newSpread'] == NEW_CARDS_LAST:
return False return False
elif self.col.conf['newSpread'] == NEW_CARDS_FIRST: elif self.col.conf['newSpread'] == NEW_CARDS_FIRST:
@ -366,7 +362,7 @@ select count() from
def _deckNewLimitSingle(self, g): def _deckNewLimitSingle(self, g):
"Limit for deck without parent limits." "Limit for deck without parent limits."
if self._cramming: if g['dyn']:
return self.reportLimit return self.reportLimit
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])
@ -434,7 +430,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
# failed # failed
else: else:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
if self._cramming: if card.odid:
print "fixme: configurable failure handling" print "fixme: configurable failure handling"
card.ivl = 1 card.ivl = 1
card.odue = self.today + 1 card.odue = self.today + 1
@ -463,9 +459,9 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
def _lrnConf(self, card): def _lrnConf(self, card):
conf = self._cardConf(card) conf = self._cardConf(card)
if card.type == 2: if card.type == 2:
return conf['lapse'] return self._lapseConf(card)
else: else:
return conf['new'] return self._newConf(card)
def _rescheduleAsRev(self, card, conf, early): def _rescheduleAsRev(self, card, conf, early):
if card.type == 2: if card.type == 2:
@ -474,20 +470,21 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
self._rescheduleNew(card, conf, early) self._rescheduleNew(card, conf, early)
card.queue = 2 card.queue = 2
card.type = 2 card.type = 2
# if we were cramming, graduating means moving back to the old deck # if we were dynamic, graduating means moving back to the old deck
if self._cramming: if card.odid:
card.did = card.odid card.did = card.odid
card.odue = 0 card.odue = 0
card.odid = 0 card.odid = 0
def _startingLeft(self, card): def _startingLeft(self, card):
return len(self._cardConf(card)['new']['delays']) conf = self._newConf(card)
return len(conf['delays'])
def _graduatingIvl(self, card, conf, early, adj=True): def _graduatingIvl(self, card, conf, early, adj=True):
if card.type == 2: if card.type == 2:
# lapsed card being relearnt # lapsed card being relearnt
if self._cramming: if card.odid:
return self._cramIvlBoost(card) return self._dynIvlBoost(card)
return card.ivl return card.ivl
if not early: if not early:
# graduate # graduate
@ -547,6 +544,8 @@ select sum(left) from
return self._deckNewLimit(did, self._deckRevLimitSingle) return self._deckNewLimit(did, self._deckRevLimitSingle)
def _deckRevLimitSingle(self, d): def _deckRevLimitSingle(self, d):
if d['dyn']:
return self.reportLimit
c = self.col.decks.confForDid(d['id']) c = self.col.decks.confForDid(d['id'])
return max(0, c['rev']['perDay'] - d['revToday'][1]) return max(0, c['rev']['perDay'] - d['revToday'][1])
@ -606,6 +605,8 @@ did = ? and queue = 2 and due <= ? %s limit ?""" % order,
def _revOrder(self, did): def _revOrder(self, did):
d = self.col.decks.confForDid(did) d = self.col.decks.confForDid(did)
if d['dyn']:
return ""
o = d['rev']['order'] o = d['rev']['order']
if o: if o:
return "order by %s" % ("ivl desc", "ivl")[o-1] return "order by %s" % ("ivl desc", "ivl")[o-1]
@ -622,7 +623,7 @@ did = ? and queue = 2 and due <= ? %s limit ?""" % order,
self._logRev(card, ease) self._logRev(card, ease)
def _rescheduleLapse(self, card): def _rescheduleLapse(self, card):
conf = self._cardConf(card)['lapse'] conf = self._lapseConf(card)
card.lapses += 1 card.lapses += 1
card.lastIvl = card.ivl card.lastIvl = card.ivl
card.ivl = self._nextLapseIvl(card, conf) card.ivl = self._nextLapseIvl(card, conf)
@ -722,64 +723,66 @@ did = ? and queue = 2 and due <= ? %s limit ?""" % order,
break break
return idealIvl + fudge return idealIvl + fudge
# Creation of cramming decks # Dynamic deck handling
########################################################################## ##########################################################################
# type is one of all, due, rev def rebuildDyn(self, did=None):
# order is one of due, added, random, relative, lapses "Rebuild a dynamic deck."
def cram(self, search, type="all", order="due", did = did or self.col.decks.selected()
limit=100, sched=[1, 10]): deck = self.col.decks.get(did)
assert deck['dyn']
# gather card ids and sort # gather card ids and sort
order = self._cramOrder(order) order = self._dynOrder(deck)
limit = " limit %d" % limit limit = " limit %d" % deck['limit']
ids = self.col.findCards(search, order=order+limit) ids = self.col.findCards(deck['search'], order=order+limit)
if not order: if deck['order'] == "random":
random.shuffle(ids) random.shuffle(ids)
# get a cram deck
did = self._cramDeck()
# move the cards over # move the cards over
self._moveToCram(did, ids) self._moveToDyn(did, ids)
# and change to our new deck # and change to our new deck
self.col.decks.select(did) self.col.decks.select(did)
def remCram(self, did): def remDyn(self, did):
self.col.db.execute(""" self.col.db.execute("""
update cards set did = odid, queue = type, due = odue, odue = 0, odid = 0, update cards set did = odid, queue = type, due = odue, odue = 0, odid = 0,
usn = ?, mod = ? where did = ?""", self.col.usn(), intTime(), did) usn = ?, mod = ? where did = ?""", self.col.usn(), intTime(), did)
def _cramOrder(self, order): def _dynOrder(self, deck):
if order == "due": o = deck['order']
return "order by c.due" if o == "oldestSeen":
elif order == "added": return "order by c.mod"
return "order by n.id" # elif o == "added":
elif order == "random": # return "order by n.id"
return "" # elif o == "random":
elif order == "relative": # return ""
pass # elif o == "relative":
elif order == "lapses": # pass
return "order by lapses desc" # elif o == "lapses":
elif order == "failed": # return "order by lapses desc"
pass # elif o == "failed":
# pass
def _cramDeck(self): def _moveToDyn(self, did, ids):
did = self.col.decks.id(_("Cram"))
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
# mark it as a cram deck
deck['cram'] = True
self.col.decks.save(deck)
return did
def _moveToCram(self, did, ids):
data = [] data = []
t = intTime(); u = self.col.usn() t = intTime(); u = self.col.usn()
for c, id in enumerate(ids): for c, id in enumerate(ids):
data.append((did, c, t, u, id)) data.append((did, c, t, u, id))
if deck['cramRev']:
# everything in the new queue
queue = "0"
else:
# due reviews stay in the review queue
queue = "(case when queue=2 and due <= %d then 2 else 0 end)"
queue %= self.today
self.col.db.executemany(""" self.col.db.executemany("""
update cards set odid = did, odue = due, did = ?, queue = 0, due = ?, update cards set
mod = ?, usn = ? where id = ?""", data) odid = (case when odid then odid else did end),
odue = (case when odue then odue else due end),
did = ?, queue = %s, due = ?, mod = ?, usn = ? where id = ?""" % queue, data)
def _cramIvlBoost(self, card): def _dynIvlBoost(self, card):
assert self._cramming and card.type == 2 assert card.odid and card.type == 2
assert card.factor assert card.factor
elapsed = card.ivl - card.odue - self.today elapsed = card.ivl - card.odue - self.today
factor = ((card.factor/1000.0)+1.2)/2.0 factor = ((card.factor/1000.0)+1.2)/2.0
@ -815,6 +818,42 @@ mod = ?, usn = ? where id = ?""", data)
def _cardConf(self, card): def _cardConf(self, card):
return self.col.decks.confForDid(card.did) return self.col.decks.confForDid(card.did)
def _newConf(self, card):
conf = self._cardConf(card)
# normal deck
if not card.odid:
return conf['new']
# dynamic deck; override some attributes, use original deck for others
oconf = self.col.decks.confForDid(card.odid)
return dict(
# original deck
ints=oconf['new']['ints'],
initialFactor=oconf['new']['initialFactor'],
# overrides
delays=conf['delays'],
separate=conf['separate'],
order=NEW_CARDS_DUE,
perDay=self.reportLimit
)
def _lapseConf(self, card):
conf = self._cardConf(card)
# normal deck
if not card.odid:
return conf['lapse']
# dynamic deck; override some attributes, use original deck for others
oconf = self.col.decks.confForDid(card.odid)
return dict(
# original deck
minInt=oconf['lapse']['minInt'],
leechFails=oconf['lapse']['leechFails'],
leechAction=oconf['lapse']['leechAction'],
# overrides
delays=conf['delays'],
mult=conf['fmult'],
)
def _deckLimit(self): def _deckLimit(self):
return ids2str(self.col.decks.active()) return ids2str(self.col.decks.active())
@ -897,7 +936,7 @@ your short-term review workload will become."""))
return self._nextLrnIvl(card, ease) return self._nextLrnIvl(card, ease)
elif ease == 1: elif ease == 1:
# lapsed # lapsed
conf = self._cardConf(card)['lapse'] conf = self._lapseConf(card)
if conf['delays']: if conf['delays']:
return conf['delays'][0]*60 return conf['delays'][0]*60
return self._nextLapseIvl(card, conf)*86400 return self._nextLapseIvl(card, conf)*86400
@ -953,6 +992,7 @@ your short-term review workload will become."""))
def forgetCards(self, ids): def forgetCards(self, ids):
"Put cards at the end of the new queue." "Put cards at the end of the new queue."
print "fixme: make sure this works with dynamic decks, and mv too"
self.col.db.execute( self.col.db.execute(
"update cards set type=0,queue=0,ivl=0 where id in "+ids2str(ids)) "update cards set type=0,queue=0,ivl=0 where id in "+ids2str(ids))
pmax = self.col.db.scalar( pmax = self.col.db.scalar(
@ -1024,4 +1064,3 @@ and due >= ?""" % scids, now, self.col.usn(), shiftby, low)
self.randomizeCards(did) self.randomizeCards(did)
else: else:
self.orderCards(did) self.orderCards(did)

View file

@ -447,7 +447,9 @@ def test_cram():
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0,0,0)
cardcopy = copy.copy(c) cardcopy = copy.copy(c)
d.sched.cram("") # create a dynamic deck and refresh it
did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did)
d.reset() d.reset()
# should appear as new in the deck list # should appear as new in the deck list
assert sorted(d.sched.deckDueList())[0][3] == 1 assert sorted(d.sched.deckDueList())[0][3] == 1
@ -478,7 +480,7 @@ def test_cram():
# and it will have moved back to the previous deck # and it will have moved back to the previous deck
assert c.did == 1 assert c.did == 1
# cram the deck again # cram the deck again
d.sched.cram("") d.sched.rebuildDyn(did)
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
# check ivls again - passing should be idempotent # check ivls again - passing should be idempotent