Skip to content

Commit

Permalink
FSRS-5 (#52)
Browse files Browse the repository at this point in the history
* start work on fsrs-5

* implement short term stability function

* update difficulty and stability in (re)learning step

* modify tests

* modify custom scheduler example in readme to use 19 parameters instead of 17 for fsrs5

* bump major version
  • Loading branch information
joshdavham authored Aug 22, 2024
1 parent 3087e1c commit b91d3d0
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 166 deletions.
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,25 @@ You can initialize the FSRS scheduler with your own custom weights as well as de
```python
f = FSRS(
w=(
1.14,
1.01,
5.44,
14.67,
5.3024,
1.5662,
1.2503,
0.0028,
1.5489,
0.1763,
0.9953,
2.7473,
0.0179,
0.3105,
0.3976,
0.0,
2.0902,
0.4197,
1.1869,
3.0412,
15.2441,
7.1434,
0.6477,
1.0007,
0.0674,
1.6597,
0.1712,
1.1178,
2.0225,
0.0904,
0.3025,
2.1214,
0.2498,
2.9466,
0.4891,
0.6468,
),
request_retention=0.85,
maximum_interval=3650,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "fsrs"
version = "2.5.1"
version = "3.0.0"
description = "Free Spaced Repetition Scheduler"
readme = "README.md"
authors = [{ name = "Jarrett Ye", email = "[email protected]" }]
Expand Down
58 changes: 43 additions & 15 deletions src/fsrs/fsrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,22 @@ def repeat(
s.easy.scheduled_days = easy_interval
s.easy.due = now + timedelta(days=easy_interval)
elif card.state == State.Learning or card.state == State.Relearning:
interval = card.elapsed_days
last_d = card.difficulty
last_s = card.stability
retrievability = self.forgetting_curve(interval, last_s)
self.next_ds(s, last_d, last_s, retrievability, card.state)

hard_interval = 0
good_interval = self.next_interval(s.good.stability)
easy_interval = max(self.next_interval(s.easy.stability), good_interval + 1)

s.schedule(now, hard_interval, good_interval, easy_interval)
elif card.state == State.Review:
interval = card.elapsed_days
last_d = card.difficulty
last_s = card.stability
retrievability = self.forgetting_curve(interval, last_s)
self.next_ds(s, last_d, last_s, retrievability)
self.next_ds(s, last_d, last_s, retrievability, card.state)

hard_interval = self.next_interval(s.hard.stability)
good_interval = self.next_interval(s.good.stability)
Expand All @@ -89,28 +94,45 @@ def init_ds(self, s: SchedulingCards) -> None:
s.easy.stability = self.init_stability(Rating.Easy)

def next_ds(
self, s: SchedulingCards, last_d: float, last_s: float, retrievability: float
self,
s: SchedulingCards,
last_d: float,
last_s: float,
retrievability: float,
state,
):
s.again.difficulty = self.next_difficulty(last_d, Rating.Again)
s.again.stability = self.next_forget_stability(last_d, last_s, retrievability)
s.hard.difficulty = self.next_difficulty(last_d, Rating.Hard)
s.hard.stability = self.next_recall_stability(
last_d, last_s, retrievability, Rating.Hard
)
s.good.difficulty = self.next_difficulty(last_d, Rating.Good)
s.good.stability = self.next_recall_stability(
last_d, last_s, retrievability, Rating.Good
)
s.easy.difficulty = self.next_difficulty(last_d, Rating.Easy)
s.easy.stability = self.next_recall_stability(
last_d, last_s, retrievability, Rating.Easy
)

if state == State.Learning or state == State.Relearning:
# compute short term stabilities
s.again.stability = self.short_term_stability(last_s, Rating.Again)
s.hard.stability = self.short_term_stability(last_s, Rating.Hard)
s.good.stability = self.short_term_stability(last_s, Rating.Good)
s.easy.stability = self.short_term_stability(last_s, Rating.Easy)

elif state == State.Review:
s.again.stability = self.next_forget_stability(
last_d, last_s, retrievability
)
s.hard.stability = self.next_recall_stability(
last_d, last_s, retrievability, Rating.Hard
)
s.good.stability = self.next_recall_stability(
last_d, last_s, retrievability, Rating.Good
)
s.easy.stability = self.next_recall_stability(
last_d, last_s, retrievability, Rating.Easy
)

def init_stability(self, r: int) -> float:
return max(self.p.w[r - 1], 0.1)

def init_difficulty(self, r: int) -> float:
return min(max(self.p.w[4] - self.p.w[5] * (r - 3), 1), 10)
# compute initial difficulty and clamp it between 1 and 10
return min(max(self.p.w[4] - math.exp(self.p.w[5] * (r - 1)) + 1, 1), 10)

def forgetting_curve(self, elapsed_days: int, stability: float) -> float:
return (1 + self.FACTOR * elapsed_days / stability) ** self.DECAY
Expand All @@ -123,7 +145,13 @@ def next_interval(self, s: float) -> int:

def next_difficulty(self, d: float, r: int) -> float:
next_d = d - self.p.w[6] * (r - 3)
return min(max(self.mean_reversion(self.p.w[4], next_d), 1), 10)

return min(
max(self.mean_reversion(self.init_difficulty(Rating.Easy), next_d), 1), 10
)

def short_term_stability(self, stability, rating):
return stability * math.exp(self.p.w[17] * (rating - 3 + self.p.w[18]))

def mean_reversion(self, init: float, current: float) -> float:
return self.p.w[7] * init + (1 - self.p.w[7]) * current
Expand Down
36 changes: 19 additions & 17 deletions src/fsrs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,23 +278,25 @@ def __init__(
w
if w is not None
else (
0.4872,
1.4003,
3.7145,
13.8206,
5.1618,
1.2298,
0.8975,
0.031,
1.6474,
0.1367,
1.0461,
2.1072,
0.0793,
0.3246,
1.587,
0.2272,
2.8755,
0.4072,
1.1829,
3.1262,
15.4722,
7.2102,
0.5316,
1.0651,
0.0234,
1.616,
0.1544,
1.0824,
1.9813,
0.0953,
0.2975,
2.2042,
0.2407,
2.9466,
0.5034,
0.6567,
)
)
self.request_retention = (
Expand Down
Loading

0 comments on commit b91d3d0

Please sign in to comment.