From b833028fc528bb3d2dc4d2a684325c4239051039 Mon Sep 17 00:00:00 2001 From: kai <52522174+ningkaiqiang@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:14:17 +0900 Subject: [PATCH] feat: linear damping (#21) --- Sources/FSRS/Algorithm/FSRSAlgorithm.swift | 14 +++- Sources/FSRS/Models/FSRSDefaults.swift | 12 +++- Tests/FSRSTests/FSRSDefaultTests.swift | 16 ++++- .../FSRSLongTermSchedulerTests.swift | 68 +++++++++++-------- Tests/FSRSTests/FSRSReschduleTests.swift | 50 +++++++------- Tests/FSRSTests/FSRSV5Tests.swift | 24 +++---- 6 files changed, 113 insertions(+), 71 deletions(-) diff --git a/Sources/FSRS/Algorithm/FSRSAlgorithm.swift b/Sources/FSRS/Algorithm/FSRSAlgorithm.swift index 7238ccd..a5942f3 100644 --- a/Sources/FSRS/Algorithm/FSRSAlgorithm.swift +++ b/Sources/FSRS/Algorithm/FSRSAlgorithm.swift @@ -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 { @@ -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)) } diff --git a/Sources/FSRS/Models/FSRSDefaults.swift b/Sources/FSRS/Models/FSRSDefaults.swift index d530aae..908f1d0 100644 --- a/Sources/FSRS/Models/FSRSDefaults.swift +++ b/Sources/FSRS/Models/FSRSDefaults.swift @@ -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 @@ -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") } } diff --git a/Tests/FSRSTests/FSRSDefaultTests.swift b/Tests/FSRSTests/FSRSDefaultTests.swift index ed53236..765765d 100644 --- a/Tests/FSRSTests/FSRSDefaultTests.swift +++ b/Tests/FSRSTests/FSRSDefaultTests.swift @@ -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) @@ -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() { diff --git a/Tests/FSRSTests/FSRSLongTermSchedulerTests.swift b/Tests/FSRSTests/FSRSLongTermSchedulerTests.swift index 07bacdd..d31e43a 100644 --- a/Tests/FSRSTests/FSRSLongTermSchedulerTests.swift +++ b/Tests/FSRSTests/FSRSLongTermSchedulerTests.swift @@ -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, ]) } @@ -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, ]) } @@ -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, ]) } @@ -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, ]) } @@ -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, ]) } @@ -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 diff --git a/Tests/FSRSTests/FSRSReschduleTests.swift b/Tests/FSRSTests/FSRSReschduleTests.swift index 7de0dbb..d437eef 100644 --- a/Tests/FSRSTests/FSRSReschduleTests.swift +++ b/Tests/FSRSTests/FSRSReschduleTests.swift @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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 @@ -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() { @@ -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 diff --git a/Tests/FSRSTests/FSRSV5Tests.swift b/Tests/FSRSTests/FSRSV5Tests.swift index 1535fdd..5b2a237 100644 --- a/Tests/FSRSTests/FSRSV5Tests.swift +++ b/Tests/FSRSTests/FSRSV5Tests.swift @@ -17,9 +17,9 @@ class FSRSV5Tests: XCTestCase { return res }() let w: [Double] = [ - 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, + 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, ] override func setUp() { @@ -63,7 +63,7 @@ class FSRSV5Tests: XCTestCase { schedulingCards = f.repeat(card: card, now: now) } - XCTAssertEqual(ivlHistory, [0, 4, 17, 62, 198, 563, 0, 0, 9, 27, 74, 190, 457]) + XCTAssertEqual(ivlHistory, [0, 4, 14, 44, 125, 328, 0, 0, 7, 16, 34, 71, 142,]) } func testMemoryState() { @@ -84,8 +84,8 @@ class FSRSV5Tests: XCTestCase { let stability = schedulingCards[Rating.good]!.card.stability let difficulty = schedulingCards[Rating.good]!.card.difficulty - XCTAssertEqual(stability, 71.4554, accuracy: 0.0001) - XCTAssertEqual(difficulty, 5.0976, accuracy: 0.0001) + XCTAssertEqual(stability, 48.4848, accuracy: 0.0001) + XCTAssertEqual(difficulty, 7.0866, accuracy: 0.0001) } func testFirstRepeat() { @@ -103,7 +103,7 @@ class FSRSV5Tests: XCTestCase { for item in schedulingCards.recordLog { let firstCard = item.value.card - stability.append(firstCard.stability.toFixedNumber(4)) + stability.append(firstCard.stability.toFixedNumber(5)) difficulty.append(firstCard.difficulty.toFixedNumber(8)) reps.append(firstCard.reps) lapses.append(firstCard.lapses) @@ -112,12 +112,12 @@ class FSRSV5Tests: XCTestCase { states.append(firstCard.state) } - XCTAssertEqual(Set(stability), Set([0.4197, 1.1869, 3.0412, 15.2441])) - XCTAssertEqual(Set(difficulty), Set([7.1434, 6.23225985, 4.49094334, 1.16304343])) + XCTAssertEqual(Set(stability), Set([0.40255, 1.18385, 3.173, 15.69105])) + XCTAssertEqual(Set(difficulty), Set([7.1949, 6.48830527, 5.28243442, 3.22450159])) XCTAssertEqual(Set(reps), Set([1, 1, 1, 1])) XCTAssertEqual(Set(lapses), Set([0, 0, 0, 0])) XCTAssertEqual(Set(elapsedDays), Set([0, 0, 0, 0])) - XCTAssertEqual(Set(scheduledDays), Set([0, 0, 0, 15])) + XCTAssertEqual(Set(scheduledDays), Set([0, 0, 0, 16])) XCTAssertEqual(Set(states), Set([.learning, .learning, .learning, .review])) } } @@ -142,8 +142,8 @@ class RetrievabilityTests: XCTestCase { func testRetrievabilityPercentageForReviewCards() { let card = FSRSDefaults().createEmptyCard(now: dateFormatter.date(from: "2023-12-01 04:00:00")!) let sc = fsrs.repeat(card: card, now: dateFormatter.date(from: "2023-12-01 04:05:00")!) - let expectedResults = ["100.00%", "100.00%", "100.00%", "90.26%"] - let expectedNumbers = [1.0, 1.0, 1.0, 0.9026208] + let expectedResults = ["100.00%", "100.00%", "100.00%", "89.83%"] + let expectedNumbers = [1.0, 1.0, 1.0, 0.89832125] for grade in Rating.allCases { if grade != .manual {