Repository: open-spaced-repetition/py-fsrs

Issue:
[TODO] linear dampling
Reference:

- https://github.com/open-spaced-repetition/ts-fsrs/pull/133
- https://github.com/open-spaced-repetition/go-fsrs/pull/28

Discussion:
@joshdavham: #67 

Solution patch:
diff --git a/README.md b/README.md
index 2cff904..3d49f88 100644
--- a/README.md
+++ b/README.md
@@ -93,25 +93,25 @@ from datetime import timedelta
 # note: the following arguments are also the defaults
 scheduler = Scheduler(
     parameters = (
-            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,
+            0.40255,
+            1.18385,
+            3.173,
+            15.69105,
+            7.1949,
+            0.5345,
+            1.4604,
+            0.0046,
+            1.54575,
+            0.1192,
+            1.01925,
+            1.9395,
+            0.11,
+            0.29605,
+            2.2698,
+            0.2315,
+            2.9898,
+            0.51655,
+            0.6621,
         ),
     desired_retention = 0.9,
     learning_steps = (timedelta(minutes=1), timedelta(minutes=10)),
diff --git a/pyproject.toml b/pyproject.toml
index 032e8a0..8f4d36b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "fsrs"
-version = "4.0.0"
+version = "4.1.0"
 description = "Free Spaced Repetition Scheduler"
 readme = "README.md"
 authors = [{ name = "Jarrett Ye", email = "jarrett.ye@outlook.com" }]
diff --git a/src/fsrs/fsrs.py b/src/fsrs/fsrs.py
index 31b280d..d247b9f 100644
--- a/src/fsrs/fsrs.py
+++ b/src/fsrs/fsrs.py
@@ -297,25 +297,25 @@ class Scheduler:
     def __init__(
         self,
         parameters: tuple[float, ...] | list[float] = (
-            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,
+            0.40255,
+            1.18385,
+            3.173,
+            15.69105,
+            7.1949,
+            0.5345,
+            1.4604,
+            0.0046,
+            1.54575,
+            0.1192,
+            1.01925,
+            1.9395,
+            0.11,
+            0.29605,
+            2.2698,
+            0.2315,
+            2.9898,
+            0.51655,
+            0.6621,
         ),
         desired_retention: float = 0.9,
         learning_steps: tuple[timedelta, ...] | list[timedelta] = (
@@ -697,13 +697,20 @@ class Scheduler:
         )
 
     def _next_difficulty(self, difficulty: float, rating: Rating) -> float:
-        def mean_reversion(arg_1: float, arg_2: float) -> float:
+        def _linear_damping(delta_difficulty: float, difficulty: float) -> float:
+            return (10.0 - difficulty) * delta_difficulty / 9.0
+
+        def _mean_reversion(arg_1: float, arg_2: float) -> float:
             return self.parameters[7] * arg_1 + (1 - self.parameters[7]) * arg_2
 
         arg_1 = self._initial_difficulty(Rating.Easy)
-        arg_2 = difficulty - (self.parameters[6] * (rating - 3))
 
-        next_difficulty = mean_reversion(arg_1, arg_2)
+        delta_difficulty = -(self.parameters[6] * (rating - 3))
+        arg_2 = difficulty + _linear_damping(
+            delta_difficulty=delta_difficulty, difficulty=difficulty
+        )
+
+        next_difficulty = _mean_reversion(arg_1=arg_1, arg_2=arg_2)
 
         # bound next_difficulty between 1 and 10
         next_difficulty = min(max(next_difficulty, 1), 10)


Test patch:
diff --git a/tests/test_fsrs.py b/tests/test_fsrs.py
index 3bb343a..dbc1a1c 100644
--- a/tests/test_fsrs.py
+++ b/tests/test_fsrs.py
@@ -4,32 +4,10 @@ import json
 import pytest
 import random
 
-test_parameters = (
-    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,
-)
-
 
 class TestPyFSRS:
     def test_review_card(self):
-        scheduler = Scheduler(parameters=test_parameters, enable_fuzzing=False)
+        scheduler = Scheduler(enable_fuzzing=False)
 
         ratings = (
             Rating.Good,
@@ -64,21 +42,21 @@ class TestPyFSRS:
         assert ivl_history == [
             0,
             4,
-            17,
-            62,
-            198,
-            563,
+            14,
+            44,
+            125,
+            328,
             0,
             0,
-            9,
-            27,
-            74,
-            190,
-            457,
+            7,
+            16,
+            34,
+            71,
+            142,
         ]
 
     def test_memo_state(self):
-        scheduler = Scheduler(parameters=test_parameters)
+        scheduler = Scheduler()
 
         ratings = (
             Rating.Again,
@@ -104,8 +82,8 @@ class TestPyFSRS:
             card=card, rating=Rating.Good, review_datetime=review_datetime
         )
 
-        assert round(card.stability) == 72
-        assert round(card.difficulty, 4) == 5.0976
+        assert round(card.stability) == 49
+        assert round(card.difficulty, 4) == 7.0866
 
     def test_repeat_default_arg(self):
         scheduler = Scheduler()
@@ -236,27 +214,6 @@ class TestPyFSRS:
 
     def test_custom_scheduler_args(self):
         scheduler = Scheduler(
-            parameters=(
-                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,
-                0.6468,
-            ),
             desired_retention=0.9,
             maximum_interval=36500,
             enable_fuzzing=False,
@@ -287,7 +244,7 @@ class TestPyFSRS:
             ivl_history.append(ivl)
             now = card.due
 
-        assert ivl_history == [0, 3, 13, 50, 163, 473, 0, 0, 12, 34, 91, 229, 541]
+        assert ivl_history == [0, 4, 14, 44, 125, 328, 0, 0, 7, 16, 34, 71, 142]
 
         # initialize another scheduler and verify parameters are properly set
         parameters2 = (
@@ -571,7 +528,7 @@ class TestPyFSRS:
         )
         interval = card.due - prev_due
 
-        assert interval.days == 16
+        assert interval.days == 15
 
         # seed 2
         random.seed(12345)
@@ -589,7 +546,7 @@ class TestPyFSRS:
         )
         interval = card.due - prev_due
 
-        assert interval.days == 15
+        assert interval.days == 14
 
     def test_no_learning_steps(self):
         scheduler = Scheduler(learning_steps=())