~kevang/mnemosyne-proj/grade-shortcuts-improvements

« back to all changes in this revision

Viewing changes to mnemosyne/mnemosyne/libmnemosyne/schedulers/SM2-mnemosyne.py

  • Committer: pbienst
  • Date: 2008-07-23 09:59:16 UTC
  • Revision ID: svn-v3-trunk0:e5e6b78b-db40-0410-9517-b98c64f8d2c1:trunk:467
Progress dump.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
##############################################################################
2
 
#
3
 
# scheduler.py <Peter.Bienstman@UGent.be>
4
 
#
5
 
##############################################################################
6
 
 
7
 
import logging, random
8
 
 
9
 
from mnemosyne.libmnemosyne.start_date import *
10
 
from mnemosyne.libmnemosyne.card import *
11
 
 
12
 
logger = logging.getLogger("mnemosyne")
13
 
 
14
 
revision_queue = []
15
 
 
16
 
 
17
 
 
18
 
##############################################################################
19
 
#
20
 
# SM2Scheduler
21
 
#
22
 
##############################################################################
23
 
 
24
 
class SM2Scheduler(Scheduler):
25
 
    
26
 
    ##########################################################################
27
 
    #
28
 
    # calculate_initial_interval
29
 
    #
30
 
    ##########################################################################
31
 
 
32
 
    def calculate_initial_interval(grade):
33
 
 
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.
36
 
 
37
 
        interval = (0, 0, 1, 3, 4, 5) [grade]
38
 
        return interval
39
 
 
40
 
 
41
 
 
42
 
    ##########################################################################
43
 
    #
44
 
    # calculate_interval_noise
45
 
    #
46
 
    ##########################################################################
47
 
 
48
 
    def calculate_interval_noise(interval):
49
 
 
50
 
        if interval == 0:
51
 
            noise = 0
52
 
        elif interval == 1:
53
 
            noise = random.randint(0,1)
54
 
        elif interval <= 10:
55
 
            noise = random.randint(-1,1)
56
 
        elif interval <= 60:
57
 
            noise = random.randint(-3,3)
58
 
        else:
59
 
            a = .05 * interval
60
 
            noise = round(random.uniform(-a,a))
61
 
 
62
 
        return noise
63
 
 
64
 
 
65
 
 
66
 
    ##########################################################################
67
 
    #
68
 
    # rebuild_revision_queue
69
 
    #
70
 
    ##########################################################################
71
 
    
72
 
    def rebuild_revision_queue(learn_ahead = False):
73
 
 
74
 
        global revision_queue
75
 
 
76
 
        revision_queue = []
77
 
 
78
 
        if not list_is_loadad():
79
 
            return
80
 
 
81
 
        update_days_since_start()
82
 
    
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.
87
 
 
88
 
        revision_queue = [i for i in cards if i.is_due_for_retention_rep()]
89
 
        revision_queue.sort(key=Card.sort_key_interval)
90
 
 
91
 
        if len(revision_queue) != 0:
92
 
            return
93
 
 
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.
99
 
 
100
 
        limit = config["grade_0_items_at_once"]
101
 
 
102
 
        grade_0 = (i for i in cards if i.is_due_for_acquisition_rep() \
103
 
                                       and i.lapses > 0 and i.grade == 0)
104
 
 
105
 
        grade_0_selected = []
106
 
 
107
 
        if limit != 0:
108
 
            for i in grade_0:
109
 
                for j in grade_0_selected:
110
 
                    if cards_are_inverses(i, j):
111
 
                        break
112
 
                else:
113
 
                    grade_0_selected.append(i)
114
 
 
115
 
                if len(grade_0_selected) == limit:
116
 
                    break
117
 
 
118
 
        grade_1 = [i for i in cards if i.is_due_for_acquisition_rep() \
119
 
                                       and i.lapses > 0 and i.grade == 1]
120
 
 
121
 
        revision_queue += 2*grade_0_selected + grade_1
122
 
 
123
 
        random.shuffle(revision_queue)
124
 
 
125
 
        if len(grade_0_selected) == limit or len(revision_queue) >= 10: 
126
 
            return
127
 
 
128
 
        # Now do the cards which have never been committed to long-term
129
 
        # memory, but which we have seen before.
130
 
 
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)
133
 
 
134
 
        grade_0_in_queue = len(grade_0_selected)
135
 
        grade_0_selected = []
136
 
 
137
 
        if limit != 0:
138
 
            for i in grade_0:
139
 
                for j in grade_0_selected:
140
 
                    if cards_are_inverses(i, j):
141
 
                        break
142
 
                else:
143
 
                    grade_0_selected.append(i)
144
 
 
145
 
                if len(grade_0_selected) + grade_0_in_queue == limit:
146
 
                    break
147
 
 
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]
150
 
 
151
 
        revision_queue += 2*grade_0_selected + grade_1
152
 
 
153
 
        random.shuffle(revision_queue)
154
 
 
155
 
        if len(grade_0_selected) + grade_0_in_queue == limit or \
156
 
           len(revision_queue) >= 10: 
157
 
            return
158
 
 
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.
163
 
 
164
 
        unseen = [i for i in cards if i.is_due_for_acquisition_rep() \
165
 
                                       and i.acq_reps <= 1]
166
 
 
167
 
        grade_0_in_queue = sum(1 for i in revision_queue if i.grade == 0)/2
168
 
        grade_0_selected = []
169
 
 
170
 
        if limit != 0 and len(unseen) != 0:    
171
 
            while True:
172
 
                if get_config("randomise_new_cards") == False:
173
 
                    new_card = unseen[0]
174
 
                else:
175
 
                    new_card = random.choice(unseen)
176
 
 
177
 
                unseen.remove(new_card)
178
 
 
179
 
                for i in grade_0_selected:
180
 
                    if cards_are_inverses(new_card, i):
181
 
                        break
182
 
                else:
183
 
                    grade_0_selected.append(new_card)
184
 
 
185
 
                if len(unseen) == 0 or \
186
 
                       len(grade_0_selected) + grade_0_in_queue == limit:
187
 
                    revision_queue += grade_0_selected
188
 
                    return      
189
 
 
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.
195
 
 
196
 
        if learn_ahead == False:
197
 
            return
198
 
        else:
199
 
            revision_queue = [i for i in cards \
200
 
                              if i.qualifies_for_learn_ahead()]
201
 
 
202
 
        revision_queue.sort(key=Card.sort_key)
203
 
 
204
 
 
205
 
 
206
 
    ##########################################################################
207
 
    #
208
 
    # in_revision_queue
209
 
    #
210
 
    ##########################################################################
211
 
 
212
 
    def in_revision_queue(card):
213
 
        return card in revision_queue
214
 
 
215
 
 
216
 
 
217
 
    ##########################################################################
218
 
    #
219
 
    # remove_from_revision_queue
220
 
    #
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.
223
 
    #
224
 
    ##########################################################################
225
 
 
226
 
    def remove_from_revision_queue(card):
227
 
 
228
 
        global revision_queue
229
 
 
230
 
        for i in revision_queue:
231
 
            if i.id == card.id:
232
 
                revision_queue.remove(i)
233
 
                return
234
 
 
235
 
 
236
 
    ##########################################################################
237
 
    #
238
 
    # get_new_question
239
 
    #
240
 
    ##########################################################################
241
 
 
242
 
    def get_new_question(learn_ahead = False):
243
 
 
244
 
        # Populate list if it is empty.
245
 
 
246
 
        if len(revision_queue) == 0:
247
 
            rebuild_revision_queue(learn_ahead)
248
 
            if len(revision_queue) == 0:
249
 
                return None
250
 
 
251
 
        # Pick the first question and remove it from the queue.
252
 
 
253
 
        card = revision_queue[0]
254
 
        revision_queue.remove(card)
255
 
 
256
 
        return card
257
 
 
258
 
 
259
 
 
260
 
    ##########################################################################
261
 
    #
262
 
    # process_answer
263
 
    #
264
 
    ##########################################################################
265
 
 
266
 
    def process_answer(card, new_grade, dry_run=False):
267
 
 
268
 
        global revision_queue, cards
269
 
 
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.
272
 
 
273
 
        if dry_run:
274
 
            card = copy.copy(card)
275
 
 
276
 
        # Calculate scheduled and actual interval, taking care of corner
277
 
        # case when learning ahead on the same day.
278
 
 
279
 
        scheduled_interval = card.next_rep    - card.last_rep
280
 
        actual_interval    = days_since_start - card.last_rep
281
 
 
282
 
        if actual_interval == 0:
283
 
            actual_interval = 1 # Otherwise new interval can become zero.
284
 
 
285
 
        if card.is_new():
286
 
 
287
 
            # The card is not graded yet, e.g. because it is imported.
288
 
 
289
 
            card.acq_reps = 1
290
 
            card.acq_reps_since_lapse = 1
291
 
 
292
 
            new_interval = calculate_initial_interval(new_grade)
293
 
 
294
 
            # Make sure the second copy of a grade 0 card doesn't show
295
 
            # up again.
296
 
 
297
 
            if not dry_run and card.grade == 0 and new_grade in [2,3,4,5]:
298
 
                for i in revision_queue:
299
 
                    if i.id == card.id:
300
 
                        revision_queue.remove(i)
301
 
                        break
302
 
 
303
 
        elif card.grade in [0,1] and new_grade in [0,1]:
304
 
 
305
 
            # In the acquisition phase and staying there.
306
 
 
307
 
            card.acq_reps += 1
308
 
            card.acq_reps_since_lapse += 1
309
 
 
310
 
            new_interval = 0
311
 
 
312
 
        elif card.grade in [0,1] and new_grade in [2,3,4,5]:
313
 
 
314
 
             # In the acquisition phase and moving to the retention phase.
315
 
 
316
 
             card.acq_reps += 1
317
 
             card.acq_reps_since_lapse += 1
318
 
 
319
 
             new_interval = 1
320
 
 
321
 
             # Make sure the second copy of a grade 0 card doesn't show
322
 
             # up again.
323
 
 
324
 
             if not dry_run and card.grade == 0:
325
 
                 for i in revision_queue:
326
 
                     if i.id == card.id:
327
 
                         revision_queue.remove(i)
328
 
                         break
329
 
 
330
 
        elif card.grade in [2,3,4,5] and new_grade in [0,1]:
331
 
 
332
 
             # In the retention phase and dropping back to the
333
 
             # acquisition phase.
334
 
 
335
 
             card.ret_reps += 1
336
 
             card.lapses += 1
337
 
             card.acq_reps_since_lapse = 0
338
 
             card.ret_reps_since_lapse = 0
339
 
 
340
 
             new_interval = 0
341
 
 
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.
344
 
 
345
 
             if not dry_run:
346
 
                 cards.remove(card)
347
 
                 cards.insert(0,card)
348
 
 
349
 
        elif card.grade in [2,3,4,5] and new_grade in [2,3,4,5]:
350
 
 
351
 
            # In the retention phase and staying there.
352
 
 
353
 
            card.ret_reps += 1
354
 
            card.ret_reps_since_lapse += 1
355
 
 
356
 
            if actual_interval >= scheduled_interval:
357
 
                if new_grade == 2:
358
 
                    card.easiness -= 0.16
359
 
                if new_grade == 3:
360
 
                    card.easiness -= 0.14
361
 
                if new_grade == 5:
362
 
                    card.easiness += 0.10
363
 
                if card.easiness < 1.3:
364
 
                    card.easiness = 1.3
365
 
 
366
 
            new_interval = 0
367
 
 
368
 
            if card.ret_reps_since_lapse == 1:
369
 
                new_interval = 6
370
 
            else:
371
 
                if new_grade == 2 or new_grade == 3:
372
 
                    if actual_interval <= scheduled_interval:
373
 
                        new_interval = actual_interval * card.easiness
374
 
                    else:
375
 
                        new_interval = scheduled_interval
376
 
 
377
 
                if new_grade == 4:
378
 
                    new_interval = actual_interval * card.easiness
379
 
 
380
 
                if new_grade == 5:
381
 
                    if actual_interval < scheduled_interval:
382
 
                        new_interval = scheduled_interval # Avoid spacing.
383
 
                    else:
384
 
                        new_interval = actual_interval * card.easiness
385
 
 
386
 
            # Shouldn't happen, but build in a safeguard.
387
 
 
388
 
            if new_interval == 0:
389
 
                logger.info("Internal error: new interval was zero.")
390
 
                new_interval = scheduled_interval
391
 
 
392
 
            new_interval = int(new_interval)
393
 
 
394
 
        # When doing a dry run, stop here and return the scheduled interval.
395
 
 
396
 
        if dry_run:
397
 
            return new_interval
398
 
 
399
 
        # Add some randomness to interval.
400
 
 
401
 
        noise = calculate_interval_noise(new_interval)
402
 
 
403
 
        # Update grade and interval.
404
 
 
405
 
        card.grade    = new_grade
406
 
        card.last_rep = days_since_start
407
 
        card.next_rep = days_since_start + new_interval + noise
408
 
 
409
 
        # Don't schedule inverse or identical questions on the same day.
410
 
 
411
 
        for j in cards:
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 \
414
 
                  and card.grade >= 2:
415
 
                    card.next_rep += 1
416
 
                    noise += 1
417
 
 
418
 
        # Create log entry.
419
 
 
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)
426
 
 
427
 
        return new_interval + noise