Skip to content

Instantly share code, notes, and snippets.

@riceissa
Last active June 2, 2025 12:23
Show Gist options
  • Save riceissa/1ead1b9881ffbb48793565ce69d7dbdd to your computer and use it in GitHub Desktop.
Save riceissa/1ead1b9881ffbb48793565ce69d7dbdd to your computer and use it in GitHub Desktop.

Revisions

  1. riceissa revised this gist Dec 15, 2023. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions anki_algorithm.py
    Original file line number Diff line number Diff line change
    @@ -12,6 +12,14 @@
    responses throughout their history), leech tracking, checking if a card from
    the same notes has been reviewed already that day, delay in response (i.e. I
    assume all cards are reviewed exactly on the day they are due).
    Update (2023-12-15): Please note that the Anki review algorithm has possibly
    changed in many ways since the time when I wrote this program (although I
    believe that Anki still uses SM2 by default, so the basic concepts should
    still be the same as what is shown below). I have sadly not had the time
    or energy to keep up with the latest changes. In particular, Anki now
    supports FSRS instead of the SM2 algorithm (which is the algorithm
    below); FSRS is not covered at all below.
    """


  2. riceissa revised this gist Jan 12, 2023. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions anki_algorithm.py
    Original file line number Diff line number Diff line change
    @@ -94,11 +94,11 @@ def schedule(card, response):
    elif card.status == 'relearning':
    if response == "again":
    card.steps_index = 0
    return minutes_to_days(LAPSE_STEPS[0])
    return minutes_to_days(LAPSES_STEPS[0])
    elif response == "good":
    card.steps_index += 1
    if card.steps_index < len(LAPSE_STEPS):
    return minutes_to_days(LAPSE_STEPS[card.steps_index])
    if card.steps_index < len(LAPSES_STEPS):
    return minutes_to_days(LAPSES_STEPS[card.steps_index])
    else:
    # we have re-graduated!
    card.status = 'learned'
  3. riceissa revised this gist Aug 11, 2021. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions anki_algorithm.py
    Original file line number Diff line number Diff line change
    @@ -74,7 +74,7 @@ def schedule(card, response):
    card.status = 'relearning'
    card.steps_index = 0
    card.ease_factor = max(130, card.ease_factor - 20)
    card.interval = card.interval * NEW_INTERVAL/100
    card.interval = max(MINIMUM_INTERVAL, card.interval * NEW_INTERVAL/100)
    return minutes_to_days(LAPSES_STEPS[0])
    elif response == "hard":
    card.ease_factor = max(130, card.ease_factor - 15)
    @@ -102,7 +102,8 @@ def schedule(card, response):
    else:
    # we have re-graduated!
    card.status = 'learned'
    card.interval = max(MINIMUM_INTERVAL, card.interval * NEW_INTERVAL/100)
    # we don't modify the interval here because that was already done when
    # going from 'learned' to 'relearning'
    return card.interval
    else:
    raise ValueError("you can't press this button / we don't know how to deal with this case")
  4. riceissa revised this gist Aug 11, 2021. 1 changed file with 2 additions and 4 deletions.
    6 changes: 2 additions & 4 deletions anki_algorithm.py
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    got from watching https://www.youtube.com/watch?v=lz60qTP2Gx0
    and https://www.youtube.com/watch?v=1XaJjbCSXT0
    and from reading
    https://apps.ankiweb.net/docs/manual.html#what-spaced-repetition-algorithm-does-anki-use
    https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html
    There is also https://github.com/dae/anki/blob/master/anki/sched.py but I find
    it really hard to understand.
    @@ -74,9 +74,7 @@ def schedule(card, response):
    card.status = 'relearning'
    card.steps_index = 0
    card.ease_factor = max(130, card.ease_factor - 20)
    # the anki manual says "the current interval is multiplied by the
    # value of new interval", but I have no idea what the "new
    # interval" is
    card.interval = card.interval * NEW_INTERVAL/100
    return minutes_to_days(LAPSES_STEPS[0])
    elif response == "hard":
    card.ease_factor = max(130, card.ease_factor - 15)
  5. riceissa revised this gist Apr 17, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion anki_algorithm.py
    Original file line number Diff line number Diff line change
    @@ -71,7 +71,7 @@ def schedule(card, response):
    raise ValueError("you can't press this button / we don't know how to deal with this case")
    elif card.status == 'learned':
    if response == "again":
    card.state = 'relearning'
    card.status = 'relearning'
    card.steps_index = 0
    card.ease_factor = max(130, card.ease_factor - 20)
    # the anki manual says "the current interval is multiplied by the
  6. riceissa created this gist Nov 22, 2019.
    136 changes: 136 additions & 0 deletions anki_algorithm.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,136 @@
    """
    This is my understanding of the Anki scheduling algorithm, which I mostly
    got from watching https://www.youtube.com/watch?v=lz60qTP2Gx0
    and https://www.youtube.com/watch?v=1XaJjbCSXT0
    and from reading
    https://apps.ankiweb.net/docs/manual.html#what-spaced-repetition-algorithm-does-anki-use
    There is also https://github.com/dae/anki/blob/master/anki/sched.py but I find
    it really hard to understand.
    Things I don't bother to implement here: the random fudge factor (that Anki
    uses to decorrelate cards that were added on the same day and have the same
    responses throughout their history), leech tracking, checking if a card from
    the same notes has been reviewed already that day, delay in response (i.e. I
    assume all cards are reviewed exactly on the day they are due).
    """


    # "New Cards" tab
    NEW_STEPS = [1, 10] # in minutes
    GRADUATING_INTERVAL = 1 # in days
    EASY_INTERVAL = 4 # in days
    STARTING_EASE = 250 # in percent

    # "Reviews" tab
    EASY_BONUS = 130 # in percent
    INTERVAL_MODIFIER = 100 # in percent
    MAXIMUM_INTERVAL = 36500 # in days

    # "Lapses" tab
    LAPSES_STEPS = [10] # in minutes
    NEW_INTERVAL = 70 # in percent
    MINIMUM_INTERVAL = 1 # in days

    class Card:
    def __init__(self):
    self.status = 'learning' # can be 'learning', 'learned', or 'relearning'
    self.steps_index = 0
    self.ease_factor = STARTING_EASE
    self.interval = None

    def __repr__(self):
    return "Card[%s; steps_idx=%s; ease=%s; interval=%s]" % (self.status,
    self.steps_index,
    self.ease_factor,
    str(self.interval))


    def schedule(card, response):
    '''response is one of "again", "hard", "good", or "easy"
    returns a result in days'''

    if card.status == 'learning':
    # for learning cards, there is no "hard" response possible
    if response == "again":
    card.steps_index = 0
    return minutes_to_days(NEW_STEPS[card.steps_index])
    elif response == "good":
    card.steps_index += 1
    if card.steps_index < len(NEW_STEPS):
    return minutes_to_days(NEW_STEPS[card.steps_index])
    else:
    # we have graduated!
    card.status = 'learned'
    card.interval = GRADUATING_INTERVAL
    return card.interval
    elif response == "easy":
    card.status = 'learned'
    card.interval = EASY_INTERVAL
    return EASY_INTERVAL
    else:
    raise ValueError("you can't press this button / we don't know how to deal with this case")
    elif card.status == 'learned':
    if response == "again":
    card.state = 'relearning'
    card.steps_index = 0
    card.ease_factor = max(130, card.ease_factor - 20)
    # the anki manual says "the current interval is multiplied by the
    # value of new interval", but I have no idea what the "new
    # interval" is
    return minutes_to_days(LAPSES_STEPS[0])
    elif response == "hard":
    card.ease_factor = max(130, card.ease_factor - 15)
    card.interval = card.interval * 1.2 * INTERVAL_MODIFIER/100
    return min(MAXIMUM_INTERVAL, card.interval)
    elif response == "good":
    card.interval = (card.interval * card.ease_factor/100
    * INTERVAL_MODIFIER/100)
    return min(MAXIMUM_INTERVAL, card.interval)
    elif response == "easy":
    card.ease_factor += 15
    card.interval = (card.interval * card.ease_factor/100
    * INTERVAL_MODIFIER/100 * EASY_BONUS/100)
    return min(MAXIMUM_INTERVAL, card.interval)
    else:
    raise ValueError("you can't press this button / we don't know how to deal with this case")
    elif card.status == 'relearning':
    if response == "again":
    card.steps_index = 0
    return minutes_to_days(LAPSE_STEPS[0])
    elif response == "good":
    card.steps_index += 1
    if card.steps_index < len(LAPSE_STEPS):
    return minutes_to_days(LAPSE_STEPS[card.steps_index])
    else:
    # we have re-graduated!
    card.status = 'learned'
    card.interval = max(MINIMUM_INTERVAL, card.interval * NEW_INTERVAL/100)
    return card.interval
    else:
    raise ValueError("you can't press this button / we don't know how to deal with this case")


    def minutes_to_days(minutes):
    return minutes / (60 * 24)


    def human_friendly_time(days):
    if not days:
    return days
    if days < 1:
    return str(round(days * 24 * 60, 2)) + " minutes"
    elif days < 30:
    return str(round(days, 2)) + " days"
    elif days < 365:
    return str(round(days / (365.25 / 12), 2)) + " months"
    else:
    return str(round(days / 365.25, 2)) + " years"


    card1 = Card()
    # responses = ["good", "good", "good", "again", "good", "good", "good"]
    responses = ["good"] * 10
    for r in responses:
    print(str(card1) + " [%s]" % r, end="→ ")
    t = schedule(card1, r)
    print(human_friendly_time(t), card1)