Skip to content

Commit

Permalink
Fuzz more intervals (#68)
Browse files Browse the repository at this point in the history
* fuzz more intervals

* modify test to pass

* reduce number of times fuzzing clause is used in the code
  • Loading branch information
joshdavham authored Dec 5, 2024
1 parent 99681fd commit 2d12f41
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 29 deletions.
51 changes: 24 additions & 27 deletions src/fsrs/fsrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ def review_card(
# calculate the card's next interval
# len(self.learning_steps) == 0: no learning steps defined so move card to Review state
# card.step > len(self.learning_steps): handles the edge-case when a card was originally scheduled with a scheduler with more
# learnning steps than the current scheduler
# learning steps than the current scheduler
if len(self.learning_steps) == 0 or card.step > len(self.learning_steps):
card.state = State.Review
card.step = None
Expand Down Expand Up @@ -461,9 +461,6 @@ def review_card(
next_interval_days = self._next_interval(stability=card.stability)
next_interval = timedelta(days=next_interval_days)

card.due = review_datetime + next_interval
card.last_review = review_datetime

elif card.state == State.Review:
assert type(card.stability) == float # mypy
assert type(card.difficulty) == float # mypy
Expand Down Expand Up @@ -505,15 +502,8 @@ def review_card(

elif rating in (Rating.Hard, Rating.Good, Rating.Easy):
next_interval_days = self._next_interval(stability=card.stability)

if self.enable_fuzzing:
next_interval_days = self._get_fuzzed_interval(next_interval_days)

next_interval = timedelta(days=next_interval_days)

card.due = review_datetime + next_interval
card.last_review = review_datetime

elif card.state == State.Relearning:
assert type(card.step) == int
assert type(card.stability) == float # mypy
Expand Down Expand Up @@ -542,9 +532,9 @@ def review_card(
)

# calculate the card's next interval
# len(self.learning_steps) == 0: no learning steps defined so move card to Review state
# card.step > len(self.learning_steps): handles the edge-case when a card was originally scheduled with a scheduler with more
# learnning steps than the current scheduler
# len(self.relearning_steps) == 0: no relearning steps defined so move card to Review state
# card.step > len(self.relearning_steps): handles the edge-case when a card was originally scheduled with a scheduler with more
# relearning steps than the current scheduler
if len(self.relearning_steps) == 0 or card.step > len(
self.relearning_steps
):
Expand Down Expand Up @@ -592,8 +582,11 @@ def review_card(
next_interval_days = self._next_interval(stability=card.stability)
next_interval = timedelta(days=next_interval_days)

card.due = review_datetime + next_interval
card.last_review = review_datetime
if self.enable_fuzzing and card.state == State.Review:
next_interval = self._get_fuzzed_interval(next_interval)

card.due = review_datetime + next_interval
card.last_review = review_datetime

return card, review_log

Expand Down Expand Up @@ -763,34 +756,36 @@ def _next_recall_stability(
* easy_bonus
)

def _get_fuzzed_interval(self, interval: int) -> int:
def _get_fuzzed_interval(self, interval: timedelta) -> timedelta:
"""
Takes the current calculated interval and adds a small amount of random fuzz to it.
For example, a card that would've been due in 50 days, after fuzzing, might be due in 49, or 51 days.
Args:
interval (int): The calculated next interval, before fuzzing.
interval (timedelta): The calculated next interval, before fuzzing.
Returns:
int: The new interval, after fuzzing.
timedelta: The new interval, after fuzzing.
"""

if interval < 2.5: # fuzz is not applied to intervals less than 2.5
interval_days = interval.days

if interval_days < 2.5: # fuzz is not applied to intervals less than 2.5
return interval

def _get_fuzz_range(interval: int) -> tuple[int, int]:
def _get_fuzz_range(interval_days: int) -> tuple[int, int]:
"""
Helper function that computes the possible upper and lower bounds of the interval after fuzzing.
"""

delta = 1.0
for fuzz_range in FUZZ_RANGES:
delta += fuzz_range["factor"] * max(
min(interval, fuzz_range["end"]) - fuzz_range["start"], 0.0
min(interval_days, fuzz_range["end"]) - fuzz_range["start"], 0.0
)

min_ivl = int(round(interval - delta))
max_ivl = int(round(interval + delta))
min_ivl = int(round(interval_days - delta))
max_ivl = int(round(interval_days + delta))

# make sure the min_ivl and max_ivl fall into a valid range
min_ivl = max(2, min_ivl)
Expand All @@ -799,12 +794,14 @@ def _get_fuzz_range(interval: int) -> tuple[int, int]:

return min_ivl, max_ivl

min_ivl, max_ivl = _get_fuzz_range(interval)
min_ivl, max_ivl = _get_fuzz_range(interval_days)

fuzzed_interval = (
fuzzed_interval_days = (
random.random() * (max_ivl - min_ivl + 1)
) + min_ivl # the next interval is a random value between min_ivl and max_ivl

fuzzed_interval = min(round(fuzzed_interval), self.maximum_interval)
fuzzed_interval_days = min(round(fuzzed_interval_days), self.maximum_interval)

fuzzed_interval = timedelta(days=fuzzed_interval_days)

return fuzzed_interval
4 changes: 2 additions & 2 deletions tests/test_fsrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def test_fuzz(self):
)
interval = card.due - prev_due

assert interval.days == 15
assert interval.days == 13

# seed 2
random.seed(12345)
Expand All @@ -546,7 +546,7 @@ def test_fuzz(self):
)
interval = card.due - prev_due

assert interval.days == 14
assert interval.days == 12

def test_no_learning_steps(self):
scheduler = Scheduler(learning_steps=())
Expand Down

0 comments on commit 2d12f41

Please sign in to comment.