Skip to content

Commit

Permalink
feat: linear damping (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
ningkaiqiang authored Dec 1, 2024
1 parent 16afd57 commit b833028
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 71 deletions.
14 changes: 11 additions & 3 deletions Sources/FSRS/Algorithm/FSRSAlgorithm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ public class FSRSAlgorithm {
* If fuzzing is disabled or ivl is less than 2.5, it returns the original interval.
* @param {number} ivl - The interval to be fuzzed.
* @param {number} elapsed_days t days since the last review
* @param {number} enable_fuzz - This adds a small random delay to the new interval time to prevent cards from sticking together and always being reviewed on the same day.
* @return {number} - The fuzzed interval.
**/
func applyFuzz(ivl: Double, elapsedDays: Double) -> Int {
Expand All @@ -134,16 +133,25 @@ public class FSRSAlgorithm {
return applyFuzz(ivl: newInterval, elapsedDays: elapsedDays)
}

/**
* @see https://github.com/open-spaced-repetition/fsrs4anki/issues/697
*/
func linearDamping(deltaD: Double, oldD: Double) -> Double {
(deltaD * (10 - oldD) / 9).toFixedNumber(8)
}

/**
* The formula used is :
* $$\text{next}_d = D - w_6 \cdot (g - 3)$$
* $$\text{delta}_d = -w_6 \cdot (g - 3)$$
* $$\text{next}_d = D + \text{linear damping}(\text{delta}_d , D)$$
* $$D^\prime(D,R) = w_7 \cdot D_0(4) +(1 - w_7) \cdot \text{next}_d$$
* @param {number} d Difficulty $$D \in [1,10]$$
* @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
* @return {number} $$\text{next}_D$$
*/
func nextDifficulty(d: Double, g: Rating) -> Double {
let nextD = d - (parameters.w[6] * Double(g.rawValue - 3))
let deltaD = -(parameters.w[6] * Double(g.rawValue - 3))
let nextD = d + linearDamping(deltaD: deltaD, oldD: d)
return constrainDifficulty(r: meanReversion(initValue: initDifficulty(.easy), current: nextD))
}

Expand Down
12 changes: 10 additions & 2 deletions Sources/FSRS/Models/FSRSDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import Foundation
public class FSRSDefaults {
var defaultRequestRetention = 0.9
var defaultMaximumInterval = 36500.0
var defaultW = [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]
var defaultW = [
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
]
var defaultEnableFuzz = false
var defaultEnableShortTerm = true

var FSRSVersion: String = "v4.4.1 using FSRS V5.0"
var FSRSVersion: String = "v5.1.0 using FSRS-5.0"

func generatorParameters(props: FSRSParameters? = nil) -> FSRSParameters {
var w = defaultW
Expand All @@ -24,6 +29,9 @@ public class FSRSDefaults {
w = p.w
w.append(0.0)
w.append(0.0)
w[4] = (w[5] * 2.0 + w[4]).toFixedNumber(8)
w[5] = (log(w[5] * 3.0 + 1.0) / 3.0).toFixedNumber(8)
w[6] = (w[6] + 0.5).toFixedNumber(8)
print("[FSRS V5]auto fill w to 19 length")
}
}
Expand Down
16 changes: 13 additions & 3 deletions Tests/FSRSTests/FSRSDefaultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class YourTestClass: XCTestCase {

func testDefaultParams() {
let expectedW = [
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,
]
let defaults = FSRSDefaults()
XCTAssertEqual(defaults.defaultRequestRetention, 0.9)
Expand All @@ -31,6 +31,16 @@ class YourTestClass: XCTestCase {
XCTAssertEqual(params.maximumInterval, defaults.defaultMaximumInterval)
XCTAssertEqual(params.w, expectedW)
XCTAssertEqual(params.enableFuzz, defaults.defaultEnableFuzz)

let params2 = defaults.generatorParameters(props: .init(w: [
0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18,
0.05, 0.34, 1.26, 0.29, 2.61,
]))

XCTAssertEqual(params2.w, [
0.4, 0.6, 2.4, 5.8, 6.81, 0.44675014, 1.36, 0.01, 1.49, 0.14, 0.94, 2.18,
0.05, 0.34, 1.26, 0.29, 2.61, 0.0, 0.0,
])
}

func testDefaultCard() {
Expand Down
68 changes: 40 additions & 28 deletions Tests/FSRSTests/FSRSLongTermSchedulerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,18 @@ class LongTermSchedulerTests: XCTestCase {
.good, .good, .good, .good, .good, .good, .again,
.again, .good, .good, .good, .good, .good
],
[3, 13, 48, 155, 445, 1158, 17, 3, 9, 27, 74, 190, 457],
[
3, 13, 48, 155, 445, 1158, 17, 3, 11, 37, 112, 307, 773,
],
[
3.0412, 13.09130698, 48.15848988, 154.93732625, 445.05562739,
1158.07779739, 16.63063166, 2.98878859, 9.46334669, 26.94735845,
73.97228121, 189.70368068, 457.43785852,
1158.07779739, 16.63063166, 3.01732209, 11.42247264, 37.37521902,
111.8752758, 306.5974569, 772.94031572,
],
[
4.49094334, 4.26664289, 4.05746029, 3.86237659, 3.68044154, 3.51076891,
5.21903785, 6.81216947, 6.43141837, 6.0763299, 5.74517439, 5.43633876,
5.14831865,
4.69833071, 5.55956298, 5.26323756, 4.98688448, 4.72915759, 4.4888015,
4.26464541,
])
}

Expand Down Expand Up @@ -124,14 +126,16 @@ class LongTermSchedulerTests: XCTestCase {
.good,
.easy,
],
[1, 2, 5, 31, 4, 6, 14, 71],
[
0.4197, 1.0344317, 4.81220091, 31.07244353, 3.94952214, 5.69573414,
14.10008388, 71.33039653,
1, 2, 6, 41, 4, 7, 21, 133
],
[
0.4197, 1.0344317, 5.5356759, 41.0033667, 4.46605519, 6.67743292,
20.88868155, 132.81849454,
],
[
7.1434, 7.67357679, 7.23476684, 5.89227986, 7.44003496, 7.95021855,
7.49276295, 6.13288703,
7.1434, 7.03653841, 6.64066485, 5.92312772, 6.44779861, 6.45995078,
6.10293922, 5.36588547,
])
}

Expand Down Expand Up @@ -178,14 +182,16 @@ class LongTermSchedulerTests: XCTestCase {
.easy,
.again,
],
[2, 7, 54, 5, 8, 22, 130, 7],
[
1.1869, 6.59167572, 53.76078737, 5.13329038, 7.91598767, 22.353464,
129.65007831, 7.25750204,
2, 7, 54, 5, 8, 26, 171, 8
],
[
1.1869, 6.59167572, 53.76078737, 5.0853693, 8.09786749, 25.52991279,
171.16195166, 8.11072373,
],
[
6.23225985, 5.89059466, 4.63870489, 6.27095095, 6.8599308, 6.47596059,
5.18461715, 6.78006872,
6.23225985, 5.89059466, 5.14583392, 5.884097, 5.99269555, 5.667177,
4.91430736, 5.71619151,
])
}

Expand Down Expand Up @@ -232,14 +238,16 @@ class LongTermSchedulerTests: XCTestCase {
.again,
.hard,
],
[3, 33, 4, 7, 24, 166, 8, 13],
[
3.0412, 32.65484522, 4.26210549, 7.16183801, 23.58957904, 166.25211957,
8.13553136, 12.60456051,
3, 33, 4, 7, 26, 193, 9, 14
],
[
3.0412, 32.65484522, 4.22256838, 7.23250123, 25.52681848, 193.36619432,
8.63899858, 14.31323884,
],
[
4.49094334, 3.33339007, 5.05361435, 5.72464269, 5.4171909, 4.19720854,
5.85921145, 6.47594255,
4.49094334, 3.69538259, 4.83221448, 5.12078462, 4.85403286, 4.07165035,
5.1050878, 5.34697075,
])
}

Expand Down Expand Up @@ -286,14 +294,16 @@ class LongTermSchedulerTests: XCTestCase {
.hard,
.good,
],
[15, 3, 6, 26, 226, 10, 17, 55],
[
15.2441, 3.25621013, 6.31387378, 25.90156323, 226.22071942, 9.55915065,
16.56937382, 55.3790909,
15, 3, 6, 27, 240, 10, 17, 60
],
[
1.16304343, 3.02954907, 3.83699941, 3.65677478, 2.55544447, 4.32810228,
5.04803013, 4.78618203,
15.2441, 3.25621013, 6.32684549, 26.56339029, 239.70462771, 9.75621519,
17.06035531, 59.59547542,
],
[
1.16304343, 2.99573557, 3.59851762, 3.43436666, 2.60045771, 4.03816348,
4.46259158, 4.24020203,
])
}

Expand Down Expand Up @@ -329,12 +339,14 @@ class LongTermSchedulerTests: XCTestCase {
stateHistory.append(card.state)
}

XCTAssertEqual(ivlHistory, [0, 4, 1, 4, 15, 0])
XCTAssertEqual(ivlHistory, [
0, 4, 1, 5, 19, 0
])
XCTAssertEqual(sHistory, [
3.0412, 3.0412, 1.21778427, 4.32308454, 14.84659978, 2.81505627,
3.0412, 3.0412, 1.21778427, 4.73753014, 19.02294877, 3.20676576,
])
XCTAssertEqual(dHistory, [
4.49094334, 4.26664289, 5.92396593, 5.60307975, 5.3038213, 6.89123851,
4.49094334, 4.26664289, 5.24649844, 4.97127357, 4.71459886, 5.57136081,
])
XCTAssertEqual(stateHistory, [
.learning, .review, .review, .review, .review, .relearning
Expand Down
50 changes: 27 additions & 23 deletions Tests/FSRSTests/FSRSReschduleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,8 @@ class FSRSReschduleTests: XCTestCase {
rating: .manual,
state: .review,
due: calendar.date(from: DateComponents(year: 2024, month: 8, day: 13, hour: 1, minute: 0))!,
stability: 18.67917062,
difficulty: 3.2828565,
stability: 18.80877052,
difficulty: 3.22450159,
elapsedDays: 1,
lastElapsedDays: 1,
scheduledDays: 19,
Expand All @@ -288,8 +288,8 @@ class FSRSReschduleTests: XCTestCase {
let nextItemExpected = RecordLogItem(
card: Card(
due: calendar.date(from: DateComponents(year: 2024, month: 9, day: 9, hour: 1, minute: 0))!,
stability: 24.84609459,
difficulty: 3.2828565,
stability: 24.7796143,
difficulty: 3.28258807,
elapsedDays: 1,
scheduledDays: 25,
reps: 4,
Expand Down Expand Up @@ -341,8 +341,8 @@ class FSRSReschduleTests: XCTestCase {
let expected = RecordLogItem(
card: Card(
due: calendar.date(from: DateComponents(year: 2024, month: 9, day: 04, hour: 17, minute: 0))!, // '2024-09-04T17:00:00.000Z'
stability: 18.67917062,
difficulty: 3.2828565,
stability: 18.80877052,
difficulty: 3.22450159,
elapsedDays: 1,
scheduledDays: 21,
reps: 3,
Expand All @@ -354,8 +354,8 @@ class FSRSReschduleTests: XCTestCase {
rating: .manual,
state: .review,
due: calendar.date(from: DateComponents(year: 2024, month: 8, day: 13, hour: 1, minute: 0))!, // '2024-08-13T01:00:00.000Z'
stability: 18.67917062,
difficulty: 3.2828565,
stability: 18.80877052,
difficulty: 3.22450159,
elapsedDays: 1,
lastElapsedDays: 1,
scheduledDays: 19,
Expand Down Expand Up @@ -401,8 +401,8 @@ class FSRSReschduleTests: XCTestCase {
let expected = RecordLogItem(
card: Card(
due: calendar.date(from: DateComponents(year: 2024, month: 9, day: 9, hour: 1, minute: 0))!, // '2024-09-09T01:00:00.000Z'
stability: 24.84609459,
difficulty: 3.2828565,
stability: 24.86663381,
difficulty: 3.22450159,
elapsedDays: 1,
scheduledDays: 25,
reps: 4,
Expand All @@ -414,8 +414,8 @@ class FSRSReschduleTests: XCTestCase {
rating: .good,
state: .review,
due: calendar.date(from: DateComponents(year: 2024, month: 8, day: 14, hour: 1, minute: 0))!, // '2024-08-14T01:00:00.000Z'
stability: 21.79806877,
difficulty: 3.2828565,
stability: 21.86357285,
difficulty: 3.22450159,
elapsedDays: 1,
lastElapsedDays: 1,
scheduledDays: 22,
Expand Down Expand Up @@ -495,9 +495,13 @@ class FSRSReschduleTests: XCTestCase {

XCTAssertNotNil(resultsShort.rescheduleItem)
XCTAssertEqual(resultsShort.collections.count, 4)
XCTAssertEqual(ivlHistoryShort, [0, 4, 15, 40])
XCTAssertEqual(sHistoryShort, [3.1262, 4.35097949, 14.94870008, 39.68105285])
XCTAssertEqual(dHistoryShort, [5.31457783, 5.26703555, 5.22060576, 5.17526243])
XCTAssertEqual(ivlHistoryShort, [0, 4, 14, 38])
XCTAssertEqual(sHistoryShort, [
3.173, 4.46685806, 14.21728391, 37.90805078,
])
XCTAssertEqual(dHistoryShort, [
5.28243442, 5.27296793, 5.26354498, 5.25416538,
])

// Switch to long-term scheduler
f.parameters.enableShortTerm = false
Expand All @@ -512,9 +516,9 @@ class FSRSReschduleTests: XCTestCase {

XCTAssertNotNil(results.rescheduleItem)
XCTAssertEqual(results.collections.count, 4)
XCTAssertEqual(ivlHistoryLong, [3, 4, 14, 39])
XCTAssertEqual(sHistoryLong, [3.1262, 3.1262, 13.89723677, 38.7694699])
XCTAssertEqual(dHistoryLong, [5.31457783, 5.26703555, 5.22060576, 5.17526243])
XCTAssertEqual(ivlHistoryLong, [3, 4, 13, 37])
XCTAssertEqual(sHistoryLong, [3.173, 3.173, 12.96611898, 36.73449305])
XCTAssertEqual(dHistoryLong, [5.28243442, 5.27296793, 5.26354498, 5.25416538])
}

func testCurrentCardEqualRescheduleCard() {
Expand All @@ -535,12 +539,12 @@ class FSRSReschduleTests: XCTestCase {
}

let currentCard = Card(
due: calendar.date(from: DateComponents(year: 2024, month: 11, day: 07))!, // 2024-11-07T00:00:00.000Z
stability: 39.68105285,
difficulty: 5.17526243,
due: calendar.date(from: DateComponents(year: 2024, month: 11, day: 05))!, // 2024-11-07T00:00:00.000Z
stability: 37.90805078,
difficulty: 5.25416538,
elapsedDays: 11,
scheduledDays: 40,
reps: 4,
scheduledDays: 9,
reps: 5,
lapses: 0,
state: .review,
lastReview: calendar.date(from: DateComponents(year: 2024, month: 10, day: 27))! // 2024-10-27T00:00:00.000Z
Expand Down
Loading

0 comments on commit b833028

Please sign in to comment.