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):
return self.col.models.get(self.note().mid)
def deckConf(self):
return self.col.decks.confForDid(self.did)
def template(self):
return self.model()['tmpls'][self.ord]
@ -140,4 +137,5 @@ lapses=?, left=?, odue=?, did=? where id = ?""",
def timeTaken(self):
"Time taken to answer card, in integer MS."
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,
'usn': 0,
'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 = {
@ -39,13 +57,6 @@ defaultConf = {
# type 0=suspend, 1=tagonly
'leechAction': 0,
},
'cram': {
'delays': [1, 5, 10],
'resched': True,
'reset': True,
'mult': 0,
'minInt': 1,
},
'rev': {
'perDay': 100,
'ease4': 1.3,
@ -91,7 +102,7 @@ class DeckManager(object):
# 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."
name = name.replace("'", "").replace('"', '')
for id, g in self.decks.items():
@ -99,7 +110,7 @@ class DeckManager(object):
return int(id)
if not create:
return None
g = copy.deepcopy(defaultDeck)
g = copy.deepcopy(type)
if "::" in name:
# not top level; ensure all parents exist
self._ensureParents(name)
@ -123,10 +134,11 @@ class DeckManager(object):
if not str(did) in self.decks:
return
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
# rather than deleting the cards
self.col.sched.remCram(did)
self.col.sched.remDyn(did)
else:
# delete children first
if childrenToo:
@ -233,7 +245,13 @@ class DeckManager(object):
return self.dconf.values()
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):
return self.dconf[str(confId)]
@ -370,3 +388,10 @@ class DeckManager(object):
for c in self.allConf():
c['usn'] = 0
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
# fixme: replace reps with deck based counts
self.reps = 0
self._cramming = False
self._updateCutoff()
def getCard(self):
@ -41,7 +40,6 @@ class Scheduler(object):
def reset(self):
deck = self.col.decks.current()
self._cramming = deck.get('cram')
self._updateCutoff()
self._resetLrn()
self._resetRev()
@ -61,10 +59,10 @@ class Scheduler(object):
card.type = 1
# init reps to graduation
card.left = self._startingLeft(card)
# cramming?
if self._cramming and card.type == 2:
# dynamic?
if card.odid and card.type == 2:
# reviews get their ivl boosted on first sight
card.ivl = self._cramIvlBoost(card)
card.ivl = self._dynIvlBoost(card)
card.odue = self.today + card.ivl
self._updateStats(card, 'new')
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()
# move any siblings to the end?
conf = self.col.decks.confForDid(self._newDids[0])
if conf['new']['separate']:
if conf['dyn'] or conf['new']['separate']:
n = len(self._newQueue)
while self._newQueue and self._newQueue[-1][1] == due:
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."
if not self.newCount:
return False
if self._cramming:
return True
if self.col.conf['newSpread'] == NEW_CARDS_LAST:
return False
elif self.col.conf['newSpread'] == NEW_CARDS_FIRST:
@ -366,7 +362,7 @@ select count() from
def _deckNewLimitSingle(self, g):
"Limit for deck without parent limits."
if self._cramming:
if g['dyn']:
return self.reportLimit
c = self.col.decks.confForDid(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1])
@ -434,7 +430,7 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
# failed
else:
card.left = self._startingLeft(card)
if self._cramming:
if card.odid:
print "fixme: configurable failure handling"
card.ivl = 1
card.odue = self.today + 1
@ -463,9 +459,9 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
def _lrnConf(self, card):
conf = self._cardConf(card)
if card.type == 2:
return conf['lapse']
return self._lapseConf(card)
else:
return conf['new']
return self._newConf(card)
def _rescheduleAsRev(self, card, conf, early):
if card.type == 2:
@ -474,20 +470,21 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
self._rescheduleNew(card, conf, early)
card.queue = 2
card.type = 2
# if we were cramming, graduating means moving back to the old deck
if self._cramming:
# if we were dynamic, graduating means moving back to the old deck
if card.odid:
card.did = card.odid
card.odue = 0
card.odid = 0
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):
if card.type == 2:
# lapsed card being relearnt
if self._cramming:
return self._cramIvlBoost(card)
if card.odid:
return self._dynIvlBoost(card)
return card.ivl
if not early:
# graduate
@ -547,6 +544,8 @@ select sum(left) from
return self._deckNewLimit(did, self._deckRevLimitSingle)
def _deckRevLimitSingle(self, d):
if d['dyn']:
return self.reportLimit
c = self.col.decks.confForDid(d['id'])
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):
d = self.col.decks.confForDid(did)
if d['dyn']:
return ""
o = d['rev']['order']
if o:
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)
def _rescheduleLapse(self, card):
conf = self._cardConf(card)['lapse']
conf = self._lapseConf(card)
card.lapses += 1
card.lastIvl = card.ivl
card.ivl = self._nextLapseIvl(card, conf)
@ -722,64 +723,66 @@ did = ? and queue = 2 and due <= ? %s limit ?""" % order,
break
return idealIvl + fudge
# Creation of cramming decks
# Dynamic deck handling
##########################################################################
# type is one of all, due, rev
# order is one of due, added, random, relative, lapses
def cram(self, search, type="all", order="due",
limit=100, sched=[1, 10]):
def rebuildDyn(self, did=None):
"Rebuild a dynamic deck."
did = did or self.col.decks.selected()
deck = self.col.decks.get(did)
assert deck['dyn']
# gather card ids and sort
order = self._cramOrder(order)
limit = " limit %d" % limit
ids = self.col.findCards(search, order=order+limit)
if not order:
order = self._dynOrder(deck)
limit = " limit %d" % deck['limit']
ids = self.col.findCards(deck['search'], order=order+limit)
if deck['order'] == "random":
random.shuffle(ids)
# get a cram deck
did = self._cramDeck()
# move the cards over
self._moveToCram(did, ids)
self._moveToDyn(did, ids)
# and change to our new deck
self.col.decks.select(did)
def remCram(self, did):
def remDyn(self, did):
self.col.db.execute("""
update cards set did = odid, queue = type, due = odue, odue = 0, odid = 0,
usn = ?, mod = ? where did = ?""", self.col.usn(), intTime(), did)
def _cramOrder(self, order):
if order == "due":
return "order by c.due"
elif order == "added":
return "order by n.id"
elif order == "random":
return ""
elif order == "relative":
pass
elif order == "lapses":
return "order by lapses desc"
elif order == "failed":
pass
def _dynOrder(self, deck):
o = deck['order']
if o == "oldestSeen":
return "order by c.mod"
# elif o == "added":
# return "order by n.id"
# elif o == "random":
# return ""
# elif o == "relative":
# pass
# elif o == "lapses":
# return "order by lapses desc"
# elif o == "failed":
# pass
def _cramDeck(self):
did = self.col.decks.id(_("Cram"))
def _moveToDyn(self, did, ids):
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 = []
t = intTime(); u = self.col.usn()
for c, id in enumerate(ids):
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("""
update cards set odid = did, odue = due, did = ?, queue = 0, due = ?,
mod = ?, usn = ? where id = ?""", data)
update cards set
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):
assert self._cramming and card.type == 2
def _dynIvlBoost(self, card):
assert card.odid and card.type == 2
assert card.factor
elapsed = card.ivl - card.odue - self.today
factor = ((card.factor/1000.0)+1.2)/2.0
@ -815,6 +818,42 @@ mod = ?, usn = ? where id = ?""", data)
def _cardConf(self, card):
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):
return ids2str(self.col.decks.active())
@ -897,7 +936,7 @@ your short-term review workload will become."""))
return self._nextLrnIvl(card, ease)
elif ease == 1:
# lapsed
conf = self._cardConf(card)['lapse']
conf = self._lapseConf(card)
if conf['delays']:
return conf['delays'][0]*60
return self._nextLapseIvl(card, conf)*86400
@ -953,6 +992,7 @@ your short-term review workload will become."""))
def forgetCards(self, ids):
"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(
"update cards set type=0,queue=0,ivl=0 where id in "+ids2str(ids))
pmax = self.col.db.scalar(
@ -1024,4 +1064,3 @@ and due >= ?""" % scids, now, self.col.usn(), shiftby, low)
self.randomizeCards(did)
else:
self.orderCards(did)

View file

@ -447,7 +447,9 @@ def test_cram():
d.reset()
assert d.sched.counts() == (0,0,0)
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()
# should appear as new in the deck list
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
assert c.did == 1
# cram the deck again
d.sched.cram("")
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
# check ivls again - passing should be idempotent