Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fuzz more intervals #68

Merged
merged 3 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading