Skip to content

Commit

Permalink
Feat/linear damping (#133)
Browse files Browse the repository at this point in the history
* Feat/linear damping

* update test

* update test

* bump version to 4.5.0

* Fix/update FSRS-4.5 param into FSRS-5 properly

* Fix/IEEE754 decimal precision

* remove unnecessary parentheses

Co-authored-by: Jarrett Ye <[email protected]>

---------

Co-authored-by: Jarrett Ye <[email protected]>
  • Loading branch information
ishiko732 and L-M-Sherlock authored Nov 30, 2024
1 parent 2845647 commit f0b1369
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 84 deletions.
26 changes: 13 additions & 13 deletions __tests__/FSRSV5.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {

describe('FSRS V5 ', () => {
const w = [
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,
]
const f: FSRS = fsrs({ w })
it('ivl_history', () => {
Expand Down Expand Up @@ -56,7 +56,7 @@ describe('FSRS V5 ', () => {
scheduling_cards = f.repeat(card, now)
}
expect(ivl_history).toEqual([
0, 4, 17, 62, 198, 563, 0, 0, 9, 27, 74, 190, 457,
0, 4, 14, 44, 125, 328, 0, 0, 7, 16, 34, 71, 142,
])
})

Expand All @@ -80,8 +80,8 @@ describe('FSRS V5 ', () => {
}

const { stability, difficulty } = scheduling_cards[Rating.Good].card
expect(stability).toBeCloseTo(71.4554, 4)
expect(difficulty).toBeCloseTo(5.0976, 4)
expect(stability).toBeCloseTo(48.4848, 4)
expect(difficulty).toBeCloseTo(7.0866, 4)
})

it('first repeat', () => {
Expand All @@ -106,12 +106,12 @@ describe('FSRS V5 ', () => {
scheduled_days.push(first_card.scheduled_days)
states.push(first_card.state)
}
expect(stability).toEqual([0.4197, 1.1869, 3.0412, 15.2441])
expect(difficulty).toEqual([7.1434, 6.23225985, 4.49094334, 1.16304343])
expect(stability).toEqual([0.40255, 1.18385, 3.173, 15.69105])
expect(difficulty).toEqual([7.1949, 6.48830527, 5.28243442, 3.22450159])
expect(reps).toEqual([1, 1, 1, 1])
expect(lapses).toEqual([0, 0, 0, 0])
expect(elapsed_days).toEqual([0, 0, 0, 0])
expect(scheduled_days).toEqual([0, 0, 0, 15])
expect(scheduled_days).toEqual([0, 0, 0, 16])
expect(states).toEqual([
State.Learning,
State.Learning,
Expand All @@ -133,8 +133,8 @@ describe('get retrievability', () => {
test('return retrievability percentage for review cards', () => {
const card = createEmptyCard('2023-12-01 04:00:00')
const sc = fsrs.repeat(card, '2023-12-01 04:05:00')
const r = ['100.00%', '100.00%', '100.00%', '90.26%']
const r_number = [1, 1, 1, 0.9026208]
const r = ['100.00%', '100.00%', '100.00%', '89.83%']
const r_number = [1, 1, 1, 0.89832125]
Grades.forEach((grade, index) => {
expect(fsrs.get_retrievability(sc[grade].card, sc[grade].card.due)).toBe(
r[index]
Expand All @@ -148,8 +148,8 @@ describe('get retrievability', () => {
test('fake the current system time', () => {
const card = createEmptyCard('2023-12-01 04:00:00')
const sc = fsrs.repeat(card, '2023-12-01 04:05:00')
const r = ['100.00%', '100.00%', '100.00%', '90.26%']
const r_number = [1, 1, 1, 0.9026208]
const r = ['100.00%', '100.00%', '100.00%', '89.83%']
const r_number = [1, 1, 1, 0.89832125]
jest.useFakeTimers()
Grades.forEach((grade, index) => {
jest.setSystemTime(sc[grade].card.due)
Expand Down
22 changes: 15 additions & 7 deletions __tests__/algorithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,16 @@ describe('next_ds', () => {
.toFixed(8)
}

const next_d = new Decimal(d)
.sub(new Decimal(params.w[6]).mul(new Decimal(g - 3)))
.toNumber()
function linear_damping(delta_d: number, old_d: number): number {
return +new Decimal(delta_d)
.mul(new Decimal(10).sub(old_d))
.div(9)
.toFixed(8)
}
const delta_d = new Decimal(-params.w[6]).mul(new Decimal(g - 3))
const next_d = +new Decimal(d)
.add(linear_damping(delta_d.toNumber(), d))
.toFixed(8)
return constrain_difficulty(mean_reversion(init_difficulty(4), next_d))
}

Expand All @@ -138,7 +145,7 @@ describe('next_ds', () => {
collection.push(d)
expected.push(expected_d)
})
expect(collection).toEqual([7.04017216, 5.9999955, 4.95981884, 3.91964218])
expect(collection).toEqual([6.60703511, 5.7994339, 4.99183271, 4.18423151])
expect(collection).toEqual(expected)
})

Expand Down Expand Up @@ -245,15 +252,16 @@ describe('next_ds', () => {
expected_next_s.push(next_s(d[index], s[index], r[index], grade))
})
expect(s_recall_collection).toEqual([
27.43740902, 15.27687386, 65.24019626, 224.35058851,
25.77614184, 14.12189062, 60.40439635, 208.97595604,
])
expect(s_recall_collection).toEqual(expected_s_recall)
expect(s_fail_collection).toEqual([
1.73909651, 2.0293769, 2.43393181, 2.95208552,
1.70284991, 1.97988166, 2.37599408, 2.88853913,
])
expect(s_fail_collection).toEqual(expected_s_fail)

expect(s_short_collection).toEqual([
2.54268521, 4.20645686, 6.95889497, 11.51235369,
2.50514262, 4.19920687, 7.03885607, 11.79877447,
])
expect(s_short_collection).toEqual(expected_s_short)

Expand Down
8 changes: 4 additions & 4 deletions __tests__/default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {

describe('default params', () => {
const expected_w = [
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,
]
expect(default_request_retention).toEqual(0.9)
expect(default_maximum_interval).toEqual(36500)
Expand Down Expand Up @@ -41,7 +41,7 @@ describe('default params', () => {
],
})
expect(params.w).toEqual([
0.4, 0.6, 2.4, 5.8, 6.81, 0.44675014, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18,
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,
])
})
Expand Down
58 changes: 29 additions & 29 deletions __tests__/impl/long-term_scheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ describe('Long-term scheduler', () => {
now = card.due
}
expect(ivl_history).toEqual([
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,
])
expect(s_history).toEqual([
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,
])
expect(d_history).toEqual([
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 @@ -94,15 +94,14 @@ describe('Long-term scheduler', () => {
d_history.push(card.difficulty)
now = card.due
}
expect(ivl_history).toEqual([1, 2, 5, 31, 4, 6, 14, 71])

expect(ivl_history).toEqual([1, 2, 6, 41, 4, 7, 21, 133])
expect(s_history).toEqual([
0.4197, 1.0344317, 4.81220091, 31.07244353, 3.94952214, 5.69573414,
14.10008388, 71.33039653,
0.4197, 1.0344317, 5.5356759, 41.0033667, 4.46605519, 6.67743292,
20.88868155, 132.81849454,
])
expect(d_history).toEqual([
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 @@ -134,15 +133,15 @@ describe('Long-term scheduler', () => {
d_history.push(card.difficulty)
now = card.due
}
expect(ivl_history).toEqual([2, 7, 54, 5, 8, 22, 130, 7])
expect(ivl_history).toEqual([2, 7, 54, 5, 8, 26, 171, 8])

expect(s_history).toEqual([
1.1869, 6.59167572, 53.76078737, 5.13329038, 7.91598767, 22.353464,
129.65007831, 7.25750204,
1.1869, 6.59167572, 53.76078737, 5.0853693, 8.09786749, 25.52991279,
171.16195166, 8.11072373,
])
expect(d_history).toEqual([
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 @@ -174,15 +173,15 @@ describe('Long-term scheduler', () => {
d_history.push(card.difficulty)
now = card.due
}
expect(ivl_history).toEqual([3, 33, 4, 7, 24, 166, 8, 13])
expect(ivl_history).toEqual([3, 33, 4, 7, 26, 193, 9, 14])

expect(s_history).toEqual([
3.0412, 32.65484522, 4.26210549, 7.16183801, 23.58957904, 166.25211957,
8.13553136, 12.60456051,
3.0412, 32.65484522, 4.22256838, 7.23250123, 25.52681848, 193.36619432,
8.63899858, 14.31323884,
])
expect(d_history).toEqual([
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,
])
})
test('test5', () => {
Expand Down Expand Up @@ -213,15 +212,15 @@ describe('Long-term scheduler', () => {
d_history.push(card.difficulty)
now = card.due
}
expect(ivl_history).toEqual([15, 3, 6, 26, 226, 10, 17, 55])
expect(ivl_history).toEqual([15, 3, 6, 27, 240, 10, 17, 60])

expect(s_history).toEqual([
15.2441, 3.25621013, 6.31387378, 25.90156323, 226.22071942, 9.55915065,
16.56937382, 55.3790909,
15.2441, 3.25621013, 6.32684549, 26.56339029, 239.70462771, 9.75621519,
17.06035531, 59.59547542,
])
expect(d_history).toEqual([
1.16304343, 3.02954907, 3.83699941, 3.65677478, 2.55544447, 4.32810228,
5.04803013, 4.78618203,
1.16304343, 2.99573557, 3.59851762, 3.43436666, 2.60045771, 4.03816348,
4.46259158, 4.24020203,
])
})

Expand Down Expand Up @@ -265,12 +264,13 @@ describe('Long-term scheduler', () => {
d_history.push(card.difficulty)
state_history.push(State[card.state])
}
expect(ivl_history).toEqual([0, 4, 1, 4, 15, 0])

expect(ivl_history).toEqual([0, 4, 1, 5, 19, 0])
expect(s_history).toEqual([
3.0412, 3.0412, 1.21778427, 4.32308454, 14.84659978, 2.81505627,
3.0412, 3.0412, 1.21778427, 4.73753014, 19.02294877, 3.20676576,
])
expect(d_history).toEqual([
4.49094334, 4.26664289, 5.92396593, 5.60307975, 5.3038213, 6.89123851,
4.49094334, 4.26664289, 5.24649844, 4.97127357, 4.71459886, 5.57136081,
])
expect(state_history).toEqual([
'Learning',
Expand Down
46 changes: 23 additions & 23 deletions __tests__/reschedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ describe('FSRS reschedule', () => {
rating: 0,
state: 2,
due: new Date(1723510800000 /**2024-08-13T01:00:00.000Z*/),
stability: 18.67917062,
difficulty: 3.2828565,
stability: 18.80877052,
difficulty: 3.22450159,
elapsed_days: 1,
last_elapsed_days: 1,
scheduled_days: 19,
Expand All @@ -299,8 +299,8 @@ describe('FSRS reschedule', () => {
const nextItemExpected = {
card: {
due: new Date(1725843600000 /**2024-09-09T01:00:00.000Z*/),
stability: 24.84609459,
difficulty: 3.2828565,
stability: 24.7796143,
difficulty: 3.28258807,
elapsed_days: 1,
scheduled_days: 25,
reps: 4,
Expand Down Expand Up @@ -348,8 +348,8 @@ describe('FSRS reschedule', () => {
const expected = {
card: {
due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z'*/),
stability: 18.67917062,
difficulty: 3.2828565,
stability: 18.80877052,
difficulty: 3.22450159,
elapsed_days: 1,
scheduled_days: 21,
reps: 3,
Expand All @@ -361,8 +361,8 @@ describe('FSRS reschedule', () => {
rating: Rating.Manual,
state: State.Review,
due: new Date(1723510800000 /**2024-08-13T01:00:00.000Z*/),
stability: 18.67917062,
difficulty: 3.2828565,
stability: 18.80877052,
difficulty: 3.22450159,
elapsed_days: 1,
last_elapsed_days: 1,
scheduled_days: 19,
Expand Down Expand Up @@ -410,8 +410,8 @@ describe('FSRS reschedule', () => {
const expected = {
card: {
due: new Date(1725843600000 /**'2024-09-09T01:00:00.000Z'*/),
stability: 24.84609459,
difficulty: 3.2828565,
stability: 24.86663381,
difficulty: 3.22450159,
elapsed_days: 1,
scheduled_days: 25,
reps: 4,
Expand All @@ -423,8 +423,8 @@ describe('FSRS reschedule', () => {
rating: Rating.Good,
state: State.Review,
due: new Date(1723597200000 /**2024-08-14T01:00:00.000Z*/),
stability: 21.79806877,
difficulty: 3.2828565,
stability: 21.86357285,
difficulty: 3.22450159,
elapsed_days: 1,
last_elapsed_days: 1,
scheduled_days: 22,
Expand Down Expand Up @@ -519,12 +519,12 @@ describe('FSRS reschedule', () => {

expect(results_short.reschedule_item).not.toBeNull()
expect(results_short.collections.length).toEqual(4)
expect(ivl_history_short).toEqual([0, 4, 15, 40])
expect(ivl_history_short).toEqual([0, 4, 14, 38])
expect(s_history_short).toEqual([
3.1262, 4.35097949, 14.94870008, 39.68105285,
3.173, 4.46685806, 14.21728391, 37.90805078,
])
expect(d_history_short).toEqual([
5.31457783, 5.26703555, 5.22060576, 5.17526243,
5.28243442, 5.27296793, 5.26354498, 5.25416538,
])

// switch long-term scheduler
Expand All @@ -543,10 +543,10 @@ describe('FSRS reschedule', () => {
)
expect(results.reschedule_item).not.toBeNull()
expect(results.collections.length).toEqual(4)
expect(ivl_history_long).toEqual([3, 4, 14, 39])
expect(s_history_long).toEqual([3.1262, 3.1262, 13.89723677, 38.7694699])
expect(ivl_history_long).toEqual([3, 4, 13, 37])
expect(s_history_long).toEqual([3.173, 3.173, 12.96611898, 36.73449305])
expect(d_history_long).toEqual([
5.31457783, 5.26703555, 5.22060576, 5.17526243,
5.28243442, 5.27296793, 5.26354498, 5.25416538,
])
})

Expand All @@ -567,12 +567,12 @@ describe('FSRS reschedule', () => {
})
}
const current_card = {
due: new Date(1730937600000 /** 2024-11-07T00:00:00.000Z */),
stability: 39.68105285,
difficulty: 5.17526243,
due: new Date(1730764800000 /** 2024-11-05T00:00:00.000Z */),
stability: 37.90805078,
difficulty: 5.25416538,
elapsed_days: 11,
scheduled_days: 40,
reps: 4,
scheduled_days: 9,
reps: 5,
lapses: 0,
state: State.Review,
last_review: Date.UTC(2024, 9, 27, 0, 0, 0),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-fsrs",
"version": "4.4.3",
"version": "4.5.0",
"description": "ts-fsrs is a versatile package based on TypeScript that supports ES modules, CommonJS, and UMD. It implements the Free Spaced Repetition Scheduler (FSRS) algorithm, enabling developers to integrate FSRS into their flashcard applications to enhance the user learning experience.",
"main": "dist/index.cjs",
"umd": "dist/index.umd.js",
Expand Down
Loading

0 comments on commit f0b1369

Please sign in to comment.