1
##############################################################################
3
# scheduler.py <Peter.Bienstman@UGent.be>
5
##############################################################################
9
from mnemosyne.libmnemosyne.start_date import *
10
from mnemosyne.libmnemosyne.card import *
12
logger = logging.getLogger("mnemosyne")
18
##############################################################################
22
##############################################################################
24
class SM2Scheduler(Scheduler):
26
##########################################################################
28
# calculate_initial_interval
30
##########################################################################
32
def calculate_initial_interval(grade):
34
# If this is the first time we grade this card, allow for slightly
35
# longer scheduled intervals, as we might know this card from before.
37
interval = (0, 0, 1, 3, 4, 5) [grade]
42
##########################################################################
44
# calculate_interval_noise
46
##########################################################################
48
def calculate_interval_noise(interval):
53
noise = random.randint(0,1)
55
noise = random.randint(-1,1)
57
noise = random.randint(-3,3)
60
noise = round(random.uniform(-a,a))
66
##########################################################################
68
# rebuild_revision_queue
70
##########################################################################
72
def rebuild_revision_queue(learn_ahead = False):
78
if not list_is_loadad():
81
update_days_since_start()
83
# Do the cards that are scheduled for today (or are overdue), but
84
# first do those that have the shortest interval, as being a day
85
# late on an interval of 2 could be much worse than being a day late
86
# on an interval of 50.
88
revision_queue = [i for i in cards if i.is_due_for_retention_rep()]
89
revision_queue.sort(key=Card.sort_key_interval)
91
if len(revision_queue) != 0:
94
# Now rememorise the cards that we got wrong during the last stage.
95
# Concentrate on only a limited number of grade 0 cards, in order to
96
# avoid too long intervals between revisions. If there are too few
97
# cards in left in the queue, append more new cards to keep some
98
# spread between these last cards.
100
limit = config["grade_0_items_at_once"]
102
grade_0 = (i for i in cards if i.is_due_for_acquisition_rep() \
103
and i.lapses > 0 and i.grade == 0)
105
grade_0_selected = []
109
for j in grade_0_selected:
110
if cards_are_inverses(i, j):
113
grade_0_selected.append(i)
115
if len(grade_0_selected) == limit:
118
grade_1 = [i for i in cards if i.is_due_for_acquisition_rep() \
119
and i.lapses > 0 and i.grade == 1]
121
revision_queue += 2*grade_0_selected + grade_1
123
random.shuffle(revision_queue)
125
if len(grade_0_selected) == limit or len(revision_queue) >= 10:
128
# Now do the cards which have never been committed to long-term
129
# memory, but which we have seen before.
131
grade_0 = (i for i in cards if i.is_due_for_acquisition_rep() \
132
and i.lapses == 0 and i.acq_reps > 1 and i.grade == 0)
134
grade_0_in_queue = len(grade_0_selected)
135
grade_0_selected = []
139
for j in grade_0_selected:
140
if cards_are_inverses(i, j):
143
grade_0_selected.append(i)
145
if len(grade_0_selected) + grade_0_in_queue == limit:
148
grade_1 = [i for i in cards if i.is_due_for_acquisition_rep() \
149
and i.lapses == 0 and i.acq_reps > 1 and i.grade == 1]
151
revision_queue += 2*grade_0_selected + grade_1
153
random.shuffle(revision_queue)
155
if len(grade_0_selected) + grade_0_in_queue == limit or \
156
len(revision_queue) >= 10:
159
# Now add some new cards. This is a bit inefficient at the moment as
160
# 'unseen' is wastefully created as opposed to being a generator
161
# expression. However, in order to use random.choice, there doesn't
162
# seem to be another option.
164
unseen = [i for i in cards if i.is_due_for_acquisition_rep() \
167
grade_0_in_queue = sum(1 for i in revision_queue if i.grade == 0)/2
168
grade_0_selected = []
170
if limit != 0 and len(unseen) != 0:
172
if get_config("randomise_new_cards") == False:
175
new_card = random.choice(unseen)
177
unseen.remove(new_card)
179
for i in grade_0_selected:
180
if cards_are_inverses(new_card, i):
183
grade_0_selected.append(new_card)
185
if len(unseen) == 0 or \
186
len(grade_0_selected) + grade_0_in_queue == limit:
187
revision_queue += grade_0_selected
190
# If we get to here, there are no more scheduled cards or new cards
191
# to learn. The user can signal that he wants to learn ahead by
192
# calling rebuild_revision_queue with 'learn_ahead' set to True.
193
# Don't shuffle this queue, as it's more useful to review the
194
# earliest scheduled cards first.
196
if learn_ahead == False:
199
revision_queue = [i for i in cards \
200
if i.qualifies_for_learn_ahead()]
202
revision_queue.sort(key=Card.sort_key)
206
##########################################################################
210
##########################################################################
212
def in_revision_queue(card):
213
return card in revision_queue
217
##########################################################################
219
# remove_from_revision_queue
221
# Remove a single instance of an card from the queue. Necessary when
222
# the queue needs to be rebuilt, and there is still a question pending.
224
##########################################################################
226
def remove_from_revision_queue(card):
228
global revision_queue
230
for i in revision_queue:
232
revision_queue.remove(i)
236
##########################################################################
240
##########################################################################
242
def get_new_question(learn_ahead = False):
244
# Populate list if it is empty.
246
if len(revision_queue) == 0:
247
rebuild_revision_queue(learn_ahead)
248
if len(revision_queue) == 0:
251
# Pick the first question and remove it from the queue.
253
card = revision_queue[0]
254
revision_queue.remove(card)
260
##########################################################################
264
##########################################################################
266
def process_answer(card, new_grade, dry_run=False):
268
global revision_queue, cards
270
# When doing a dry run, make a copy to operate on. Note that this
271
# leaves the original in cards and the reference in the GUI intact.
274
card = copy.copy(card)
276
# Calculate scheduled and actual interval, taking care of corner
277
# case when learning ahead on the same day.
279
scheduled_interval = card.next_rep - card.last_rep
280
actual_interval = days_since_start - card.last_rep
282
if actual_interval == 0:
283
actual_interval = 1 # Otherwise new interval can become zero.
287
# The card is not graded yet, e.g. because it is imported.
290
card.acq_reps_since_lapse = 1
292
new_interval = calculate_initial_interval(new_grade)
294
# Make sure the second copy of a grade 0 card doesn't show
297
if not dry_run and card.grade == 0 and new_grade in [2,3,4,5]:
298
for i in revision_queue:
300
revision_queue.remove(i)
303
elif card.grade in [0,1] and new_grade in [0,1]:
305
# In the acquisition phase and staying there.
308
card.acq_reps_since_lapse += 1
312
elif card.grade in [0,1] and new_grade in [2,3,4,5]:
314
# In the acquisition phase and moving to the retention phase.
317
card.acq_reps_since_lapse += 1
321
# Make sure the second copy of a grade 0 card doesn't show
324
if not dry_run and card.grade == 0:
325
for i in revision_queue:
327
revision_queue.remove(i)
330
elif card.grade in [2,3,4,5] and new_grade in [0,1]:
332
# In the retention phase and dropping back to the
337
card.acq_reps_since_lapse = 0
338
card.ret_reps_since_lapse = 0
342
# Move this card to the front of the list, to have precedence
343
# over cards which are still being learned for the first time.
349
elif card.grade in [2,3,4,5] and new_grade in [2,3,4,5]:
351
# In the retention phase and staying there.
354
card.ret_reps_since_lapse += 1
356
if actual_interval >= scheduled_interval:
358
card.easiness -= 0.16
360
card.easiness -= 0.14
362
card.easiness += 0.10
363
if card.easiness < 1.3:
368
if card.ret_reps_since_lapse == 1:
371
if new_grade == 2 or new_grade == 3:
372
if actual_interval <= scheduled_interval:
373
new_interval = actual_interval * card.easiness
375
new_interval = scheduled_interval
378
new_interval = actual_interval * card.easiness
381
if actual_interval < scheduled_interval:
382
new_interval = scheduled_interval # Avoid spacing.
384
new_interval = actual_interval * card.easiness
386
# Shouldn't happen, but build in a safeguard.
388
if new_interval == 0:
389
logger.info("Internal error: new interval was zero.")
390
new_interval = scheduled_interval
392
new_interval = int(new_interval)
394
# When doing a dry run, stop here and return the scheduled interval.
399
# Add some randomness to interval.
401
noise = calculate_interval_noise(new_interval)
403
# Update grade and interval.
405
card.grade = new_grade
406
card.last_rep = days_since_start
407
card.next_rep = days_since_start + new_interval + noise
409
# Don't schedule inverse or identical questions on the same day.
412
if (j.q == card.q and j.a == card.a) or cards_are_inverses(card,j):
413
if j != card and j.next_rep == card.next_rep \
420
logger.info("R %s %d %1.2f | %d %d %d %d %d | %d %d | %d %d | %1.1f",
421
card.id, card.grade, card.easiness,
422
card.acq_reps, card.ret_reps, card.lapses,
423
card.acq_reps_since_lapse, card.ret_reps_since_lapse,
424
scheduled_interval, actual_interval,
425
new_interval, noise, thinking_time)
427
return new_interval + noise