Skip to content

Instantly share code, notes, and snippets.

@RickCarlino
Last active February 3, 2024 04:45
Show Gist options
  • Save RickCarlino/673761bcbfcced91de1e2c4572694240 to your computer and use it in GitHub Desktop.
Save RickCarlino/673761bcbfcced91de1e2c4572694240 to your computer and use it in GitHub Desktop.
(NOT PEER REVIEWED) My attempt to understand FSRS in Typescript
==== Simulate same grade 5 times in a row ====
====== AGAIN ======
┌─────────┬───────────────────┬─────────────────────┬─────────────────────┐
│ (index) │ D │ S │ I │
├─────────┼───────────────────┼─────────────────────┼─────────────────────┤
│ 0 │ 3.05 │ 0.4 │ 0.3999999999999999 │
│ 1 │ 4.771599999999999 │ 0.2771731229713308 │ 0.2771731229713307 │
│ 2 │ 6.475983999999999 │ 0.19535411986485177 │ 0.19535411986485174 │
│ 3 │ 8.16332416 │ 0.13925231677732938 │ 0.13925231677732935 │
│ 4 │ 9.833790918400002 │ 0.09996923095782838 │ 0.09996923095782835 │
└─────────┴───────────────────┴─────────────────────┴─────────────────────┘
====== HARD ======
┌─────────┬────────────────────┬───────────────────┬────────────────────┐
│ (index) │ D │ S │ I │
├─────────┼────────────────────┼───────────────────┼────────────────────┤
│ 0 │ 3.9899999999999998 │ 0.6 │ 0.5999999999999999 │
│ 1 │ 4.8508 │ 1.102602963508004 │ 1.1026029635080037 │
│ 2 │ 5.702991999999999 │ 1.833249708167009 │ 1.8332497081670085 │
│ 3 │ 6.546662079999999 │ 2.784407563524567 │ 2.784407563524566 │
│ 4 │ 7.381895459199999 │ 3.891407699993187 │ 3.8914076999931857 │
└─────────┴────────────────────┴───────────────────┴────────────────────┘
====== GOOD ======
┌─────────┬──────┬────────────────────┬────────────────────┐
│ (index) │ D │ S │ I │
├─────────┼──────┼────────────────────┼────────────────────┤
│ 0 │ 4.93 │ 2.4 │ 2.3999999999999995 │
│ 1 │ 4.93 │ 8.035970512572042 │ 8.035970512572039 │
│ 2 │ 4.93 │ 23.969794373339646 │ 23.96979437333964 │
│ 3 │ 4.93 │ 64.75459555875548 │ 64.75459555875547 │
│ 4 │ 4.93 │ 160.62393968568293 │ 160.6239396856829 │
└─────────┴──────┴────────────────────┴────────────────────┘
====== EASY ======
┌─────────┬────────────────────┬────────────────────┬────────────────────┐
│ (index) │ D │ S │ I │
├─────────┼────────────────────┼────────────────────┼────────────────────┤
│ 0 │ 5.869999999999999 │ 5.8 │ 5.799999999999998 │
│ 1 │ 5.009199999999998 │ 36.807857261400166 │ 36.80785726140016 │
│ 2 │ 4.157007999999998 │ 210.34552398723548 │ 210.34552398723542 │
│ 3 │ 3.3133379199999977 │ 1083.114431679571 │ 1083.1144316795708 │
│ 4 │ 2.4781045407999978 │ 5044.024641286409 │ 5044.024641286408 │
└─────────┴────────────────────┴────────────────────┴────────────────────┘
// SEE: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm
// ======= EXAMPLE USAGE (I think?) =======
type Card = { D: Difficulty; S: Stability; I: IntervalDays; };
/** For simplicity, a deck will have the same requested retention rate. */
export function createDeck(requestedRetentionRate: number) {
return {
newCard(grade: Grade): Card {
const S = initialStability(grade);
return {
D: initialDifficulty(grade),
S,
I: nextInterval(requestedRetentionRate, S),
};
},
gradeCard(grade: Grade, daysPassed: number, card: Card): Card {
const fn =
grade === Grade.AGAIN
? nextStabilityAfterForgetting
: nextStabilityAfterRecall;
const D = nextDifficulty(card.D, grade);
const S = fn(D, card.S, requestedRetentionRate, grade);
return {
D,
S,
I: nextInterval(requestedRetentionRate, S),
};
},
};
}
const { newCard, gradeCard } = createDeck(0.9);
console.group("==== Simulate same grade 5 times in a row ====");
[Grade.AGAIN, Grade.HARD, Grade.GOOD, Grade.EASY].map((grade) => {
console.group(`====== ${Grade[grade]} ======`);
const card1 = newCard(grade);
const card2 = gradeCard(grade, card1.I, card1);
const card3 = gradeCard(grade, card2.I, card2);
const card4 = gradeCard(grade, card3.I, card3);
const card5 = gradeCard(grade, card4.I, card4);
console.table([card1, card2, card3, card4, card5]);
console.groupEnd();
});
console.groupEnd();
enum Grade {
AGAIN = 1,
HARD = 2,
GOOD = 3,
EASY = 4,
}
type Difficulty = number; // 1..10
type Stability = number;
type DaysSinceReview = number;
type Retrievability = number; // probability of recall
type IntervalDays = number;
// Constants for FSRS-4.5
const DECAY = -0.5;
const FACTOR = 19 / 81;
const 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,
];
// A simple Left vs. right assertion for numbers:
function compare(a: number, b: number, epsilon: number, message: string) {
if (Math.abs(a - b) > epsilon) {
console.table({
a,
b,
diff: Math.abs(a - b),
epsilon,
});
console.error(message);
process.exit(1);
}
}
/**
* Calculates the retrievability after t days since the last review.
* @param {number} t - The number of days since the last review.
* @param {number} S - Stability (interval when R=90%).
* @returns {number} - Retrievability (probability of recall).
*/
function retrievability(t: DaysSinceReview, S: Stability): Retrievability {
// Should return 0.9 when t = S
return Math.pow(1 + FACTOR * (t / S), DECAY);
}
compare(retrievability(1, 1), 0.9, 0.0, "R must equal 0.9 when t = S");
/**
* Calculates the next interval based on requested retention.
* @param {number} r - The requested retention rate.
* @param {number} S - Current stability.
* @returns {number} - The next interval in days.
*/
function nextInterval(R: Retrievability, S: Stability): IntervalDays {
// I(r, S) = S when R = 0.9
return (S / FACTOR) * (Math.pow(R, 1 / DECAY) - 1);
}
compare(nextInterval(0.9, 2), 2, 0.01, "Expected I(r, S) = S when R = 0.9");
/**
* Calculates initial stability after the first rating.
* @param {number} G - Grade (1: again, 2: hard, 3: good, 4: easy).
* @returns {number} - Initial stability.
*/
function initialStability(G: Grade): Stability {
return w[G - 1];
}
compare(
initialStability(Grade.AGAIN),
w[0],
0.0,
"AGAIN should have stability of w[0]"
);
compare(
initialStability(Grade.EASY),
w[3],
0.0,
"AGAIN should have stability of w[3]"
);
/**
* Calculates initial difficulty after the first rating.
* @param {number} G - Grade (1: again, 2: hard, 3: good, 4: easy).
* @returns {number} - Initial difficulty.
*/
function initialDifficulty(G: Grade): Difficulty {
return w[4] + (G - 3) * w[5];
}
compare(
initialDifficulty(Grade.GOOD),
w[4],
0.0,
"GOOD should have initial difficulty of w[4]"
);
/**
* Calculates new difficulty after review.
* @param {number} D - Current difficulty.
* @param {number} G - Grade (1: again, 2: hard, 3: good, 4: easy).
* @returns {number} - New difficulty.
*/
function nextDifficulty(D: Difficulty, G: Grade): Difficulty {
return w[7] * initialDifficulty(3) + (1 - w[7]) * (D - w[6] * (G - 3));
}
// Truth table for nextDifficulty:
// |1, 2, 3, 4, 5, 6, 7, 8, 9, 10
// ------+------------------------------------------------------------
// AGAIN |2.74, 3.73, 4.72, 5.71, 6.70, 7.69, 8.68, 9.67, 10.66, 11.65
// HARD |1.89, 2.88, 3.87, 4.86, 5.85, 6.84, 7.83, 8.82, 9.81, 10.80
// GOOD |1.04, 2.03, 3.02, 4.01, 5.00, 5.99, 6.98, 7.97, 8.96, 9.95
// EASY |0.19, 1.18, 2.17, 3.16, 4.15, 5.14, 6.13, 7.12, 8.11, 9.10
compare(
nextDifficulty(10, Grade.EASY),
9.1,
0.1,
"Not certain this is correct..."
);
compare(
nextDifficulty(5, Grade.HARD),
5.85,
0.1,
"Not certain this is correct..."
);
/**
* The stability after recall. Thanks, ts-fsrs.
* @param {number} d - Difficulty.
* @param {number} s - Current stability.
* @param {number} r - Retrievability.
* @param {number} g - Grade (1: again, 2: hard, 3: good, 4: easy).
* @returns {number} - New stability after recall.
*/
function nextStabilityAfterRecall(
d: number,
s: number,
r: number,
g: Grade
): Stability {
const hard_penalty = Grade.HARD === g ? w[15] : 1;
const easy_bound = Grade.EASY === g ? w[16] : 1;
return (
s *
(1 +
Math.exp(w[8]) *
(11 - d) *
Math.pow(s, -w[9]) *
(Math.exp((1 - r) * w[10]) - 1) *
hard_penalty *
easy_bound)
);
}
/**
* Calculates stability after forgetting.
* @param {number} d - Difficulty.
* @param {number} s - Current stability.
* @param {number} r - Retrievability.
* @returns {number} - New stability after forgetting.
*/
function nextStabilityAfterForgetting(
d: number,
s: number,
r: number
): Stability {
return (
w[11] *
Math.pow(d, -w[12]) *
(Math.pow(s + 1, w[13]) - 1) *
Math.exp((1 - r) * w[14])
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment