Skip to content

Commit

Permalink
Feat/add next_state method (#146)
Browse files Browse the repository at this point in the history
* Feat/add next_state method

* remove test

* confirm the boundaries&clip PLS

* bump version to 4.6.0

* Update the boundaries

* Update src/fsrs/algorithm.ts

Co-authored-by: Luc Mcgrady <[email protected]>

* Update src/fsrs/algorithm.ts

Co-authored-by: Luc Mcgrady <[email protected]>

* Update __tests__/algorithm.test.ts

Co-authored-by: Luc Mcgrady <[email protected]>

* Update __tests__/algorithm.test.ts

Co-authored-by: Luc Mcgrady <[email protected]>

* Update __tests__/algorithm.test.ts

Co-authored-by: Luc Mcgrady <[email protected]>

* Update __tests__/algorithm.test.ts

Co-authored-by: Luc Mcgrady <[email protected]>

* Update __tests__/algorithm.test.ts

Co-authored-by: Luc Mcgrady <[email protected]>

---------

Co-authored-by: Luc Mcgrady <[email protected]>
  • Loading branch information
ishiko732 and Luc-Mcgrady authored Jan 6, 2025
1 parent cdd9158 commit 8355606
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 4 deletions.
68 changes: 68 additions & 0 deletions __tests__/algorithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,71 @@ describe('change Params', () => {
}).toThrow('Requested retention rate should be in the range (0,1]')
})
})

describe('next_state', () => {
it('next_state not NaN', () => {
const f = fsrs()
const next_state = f.next_state(
{ stability: 0, difficulty: 0 },
1,
1 /** Again */
)

expect(Number.isNaN(next_state.stability)).toBe(false)
expect(next_state).toEqual(f.next_state(null, 1, 1 /** Again */))
expect(next_state).toEqual(
f.next_state({ difficulty: 0, stability: 0 }, 1, 1 /** Again */)
)
})

it('invalid memory state', () => {
const f = fsrs()

const init = f.next_state(null, 0, 3 /** Good */)
// d<1
expect(() => {
f.next_state(
{ stability: init.stability, difficulty: 0 },
1,
1 /** Again */
)
}).toThrow(/^Invalid memory state/)

// s<0.01
expect(() => {
f.next_state(
{ stability: 0, difficulty: init.stability },
1,
1 /** Again */
)
}).toThrow(/^Invalid memory state/)

// t<0
expect(() => {
f.next_state(
{ stability: 0, difficulty: 0 },
-1 /** invalid delta_t */,
1 /** Again */
)
}).toThrow(/^Invalid delta_t/)

// g<0
expect(() => {
f.next_state(init, 1, -1 /** invalid grade */)
}).toThrow(/^Invalid grade/)

// g>4
expect(() => {
f.next_state(init, 1, 5 /** invalid grade */)
}).toThrow(/^Invalid grade/)
})

it('clamped s', () => {
const f = fsrs()
const state = { difficulty: 9.98210112, stability: 0.01020119 }

const newState = f.next_state(state, 1, 1)

expect(newState.stability).toBeGreaterThanOrEqual(0.01)
})
})
144 changes: 143 additions & 1 deletion __tests__/fixed/calc-elapsed-days.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { createEmptyCard, fsrs, Grade, Rating } from '../../src/fsrs'
import {
createEmptyCard,
dateDiffInDays,
fsrs,
FSRSHistory,
Grade,
Rating,
State,
FSRSState,
} from '../../src/fsrs'

/**
* @see https://forums.ankiweb.net/t/feature-request-estimated-total-knowledge-over-time/53036/58?u=l.m.sherlock
Expand All @@ -24,3 +33,136 @@ test('TS-FSRS-Simulator', () => {
expect(card.stability).toBeCloseTo(expected[i], 4)
}
})


test('SSE use next_state', () => {
const f = fsrs({
w: [
0.4911, 4.5674, 24.8836, 77.045, 7.5474, 0.1873, 1.7732, 0.001, 1.1112,
0.152, 0.5728, 1.8747, 0.1733, 0.2449, 2.2905, 0.0, 2.9898, 0.0883,
0.9033,
],
})

const rids = [
1698678054940 /**2023-10-30T15:00:54.940Z */,
1698678126399 /**2023-10-30T15:02:06.399Z */,
1698688771401 /**2023-10-30T17:59:31.401Z */,
1698688837021 /**2023-10-30T18:00:37.021Z */,
1698688916440 /**2023-10-30T18:01:56.440Z */,
1698698192380 /**2023-10-30T20:36:32.380Z */,
1699260169343 /**2023-11-06T08:42:49.343Z */,
1702718934003 /**2023-12-16T09:28:54.003Z */,
1704910583686 /**2024-01-10T18:16:23.686Z */,
1713000017248 /**2024-04-13T09:20:17.248Z */,
]
const ratings: Rating[] = [3, 3, 1, 3, 3, 3, 0, 3, 0, 3]
// 0,0,0,0,0,0,47,119
let last = new Date(rids[0])
let memoryState: FSRSState | null = null
for (let i = 0; i < rids.length; i++) {
const current = new Date(rids[i])
const rating = ratings[i]
const delta_t = dateDiffInDays(last, current)
const nextStates = f.next_state(memoryState, delta_t, rating)
if (rating !== 0) {
last = new Date(rids[i])
}

console.debug(
rids[i + 1],
rids[i],
delta_t,
+nextStates.stability.toFixed(2),
+nextStates.difficulty.toFixed(2)
)
memoryState = nextStates
}
expect(memoryState?.stability).toBeCloseTo(71.77)
})

test.skip('SSE 71.77', () => {
const f = fsrs({
w: [
0.4911, 4.5674, 24.8836, 77.045, 7.5474, 0.1873, 1.7732, 0.001, 1.1112,
0.152, 0.5728, 1.8747, 0.1733, 0.2449, 2.2905, 0.0, 2.9898, 0.0883,
0.9033,
],
})

const rids = [
1698678054940 /**2023-10-30T15:00:54.940Z */,
1698678126399 /**2023-10-30T15:02:06.399Z */,
1698688771401 /**2023-10-30T17:59:31.401Z */,
1698688837021 /**2023-10-30T18:00:37.021Z */,
1698688916440 /**2023-10-30T18:01:56.440Z */,
1698698192380 /**2023-10-30T20:36:32.380Z */,
1699260169343 /**2023-11-06T08:42:49.343Z */,
1702718934003 /**2023-12-16T09:28:54.003Z */,
1704910583686 /**2024-01-10T18:16:23.686Z */,
1713000017248 /**2024-04-13T09:20:17.248Z */,
]
const ratings: Rating[] = [3, 3, 1, 3, 3, 3, 0, 3, 0, 3]

const expected = [
{
elapsed_days: 0,
s: 24.88,
d: 7.09,
},
{
elapsed_days: 0,
s: 26.95,
d: 7.09,
},
{
elapsed_days: 0,
s: 24.46,
d: 8.24,
},
{
elapsed_days: 0,
s: 26.48,
d: 8.24,
},
{
elapsed_days: 0,
s: 28.69,
d: 8.23,
},
{
elapsed_days: 0,
s: 31.08,
d: 8.23,
},
{
elapsed_days: 0,
s: 47.44,
d: 8.23,
},
{
elapsed_days: 119,
s: 71.77,
d: 8.23,
},
]

let card = createEmptyCard(new Date(rids[0]))

for (let i = 0; i < rids.length; i++) {
const rating = ratings[i]
if (rating == 0) {
continue
}

const now = new Date(rids[i])
const log = f.next(card, now, rating)
card = log.card
console.debug(i + 1)
expect(card.elapsed_days).toBe(expected[i].elapsed_days)
expect(card.stability).toBeCloseTo(expected[i].s, 2)
expect(card.difficulty).toBeCloseTo(expected[i].d, 2)
}

expect(card.stability).toBeCloseTo(71.77)
})
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.5.2",
"version": "4.6.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
58 changes: 57 additions & 1 deletion src/fsrs/algorithm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generatorParameters } from './default'
import { FSRSParameters, Grade, Rating } from './models'
import { FSRSParameters, FSRSState, Grade, Rating } from './models'
import type { int } from './types'
import { clamp, get_fuzz_range } from './help'
import { alea } from './alea'
Expand Down Expand Up @@ -275,4 +275,60 @@ export class FSRSAlgorithm {
forgetting_curve(elapsed_days: number, stability: number): number {
return +Math.pow(1 + (FACTOR * elapsed_days) / stability, DECAY).toFixed(8)
}

/**
* Calculates the next state of memory based on the current state, time elapsed, and grade.
*
* @param memory_state - The current state of memory, which can be null.
* @param t - The time elapsed since the last review.
* @param {Rating} g Grade (Rating[0.Manual,1.Again,2.Hard,3.Good,4.Easy])
* @returns The next state of memory with updated difficulty and stability.
*/
next_state(memory_state: FSRSState | null, t: number, g: number): FSRSState {
const { difficulty: d, stability: s } = memory_state ?? {
difficulty: 0,
stability: 0,
}
if (t < 0) {
throw new Error(`Invalid delta_t "${t}"`)
}
if (g < 0 || g > 4) {
throw new Error(`Invalid grade "${g}"`)
}
if (d === 0 && s === 0) {
return {
difficulty: this.init_difficulty(g),
stability: this.init_stability(g),
}
}
if (g === 0) {
return {
difficulty: d,
stability: s,
}
}
if (d < 1 || s < 0.01) {
throw new Error(`Invalid memory state { difficulty: ${d}, stability: ${s} }`)
}
const r = this.forgetting_curve(t, s)
const s_after_success = this.next_recall_stability(d, s, r, g)
const s_after_fail = this.next_forget_stability(d, s, r)
const s_after_short_term = this.next_short_term_stability(s, g)
let new_s = s_after_success
if (g === 1) {
let [w_17, w_18] = [0, 0]
if (this.param.enable_short_term) {
w_17 = this.param.w[17]
w_18 = this.param.w[18]
}
const next_s_min = s / Math.exp(w_17 * w_18)
new_s = clamp(next_s_min, 0.01, s_after_fail)
}
if (t === 0 && this.param.enable_short_term) {
new_s = s_after_short_term
}

const new_d = this.next_difficulty(d, g)
return { difficulty: new_d, stability: new_s }
}
}
2 changes: 1 addition & 1 deletion src/fsrs/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const default_w = [
export const default_enable_fuzz = false
export const default_enable_short_term = true

export const FSRSVersion: string = 'v4.5.2 using FSRS-5.0'
export const FSRSVersion: string = 'v4.6.0 using FSRS-5.0'

export const generatorParameters = (
props?: Partial<FSRSParameters>
Expand Down
1 change: 1 addition & 0 deletions src/fsrs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
DateInput,
FSRSReview,
FSRSHistory,
FSRSState
} from './models'
export { State, Rating } from './models'

Expand Down
5 changes: 5 additions & 0 deletions src/fsrs/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,8 @@ export type FSRSHistory = Partial<
review: DateInput | Date
}
)

export interface FSRSState {
stability: number
difficulty: number
}

0 comments on commit 8355606

Please sign in to comment.