Skip to content

Commit

Permalink
Feat/long-term scheduler (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: Asuka Minato <[email protected]>
Co-authored-by: Jarrett Ye <[email protected]>
  • Loading branch information
3 people authored Oct 6, 2024
1 parent 1bdd482 commit 2e329b6
Show file tree
Hide file tree
Showing 11 changed files with 816 additions and 324 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fsrs"
version = "0.3.0"
version = "1.0.0"
edition = "2021"

[dependencies]
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ Quickstart:

```rust
use chrono::Utc;
use fsrs::{FSRS, Card, Rating::Easy};
use fsrs::{FSRS, Card, Rating};

fn main() {
let fsrs = FSRS::default();
let card = Card::new();

let scheduled_cards = fsrs.schedule(card, Utc::now());

let updated_card = scheduled_cards.select_card(Easy);

println!("{:?}", updated_card.log);
let scheduleing_card = fsrs.repeat(card, Utc::now());
for rating in Rating::iter() {
let item = scheduleing_card.get(rating).unwrap().to_owned();
println!("{:?}", item.card);
println!("{:?}", item.review_log);
}
}
```

Expand Down
227 changes: 19 additions & 208 deletions src/algo.rs
Original file line number Diff line number Diff line change
@@ -1,223 +1,34 @@
use crate::models::Rating;
use crate::models::Rating::{Again, Easy, Good, Hard};
use crate::models::State::{Learning, New, Relearning, Review};
use crate::models::*;
use chrono::{DateTime, Duration, Utc};
use std::cmp;
use crate::models::{Card, Rating, RecordLog, SchedulingInfo};
use crate::parameters::Parameters;
use crate::scheduler_basic::BasicScheduler;
use crate::scheduler_longterm::LongtermScheduler;
use crate::ImplScheduler;

use chrono::{DateTime, Utc};

#[derive(Debug, Default, Clone, Copy)]
pub struct FSRS {
params: Parameters,
parameters: Parameters,
}

impl FSRS {
pub const fn new(params: Parameters) -> Self {
Self { params }
pub const fn new(parameters: Parameters) -> Self {
Self { parameters }
}

pub fn schedule(&self, mut card: Card, now: DateTime<Utc>) -> ScheduledCards {
card.reps += 1;
card.previous_state = card.state;

card.elapsed_days = match card.state {
New => 0,
_ => (now - card.last_review).num_days(),
};
card.last_review = now;

let mut output_cards = ScheduledCards::new(&card, now);

match card.state {
New => {
self.init_difficulty_stability(&mut output_cards);

self.set_due(&mut output_cards, Again, Duration::minutes(1));
self.set_due(&mut output_cards, Hard, Duration::minutes(5));
self.set_due(&mut output_cards, Good, Duration::minutes(10));

let easy_interval = self.next_interval(&mut output_cards, Easy).unwrap();
self.set_scheduled_days(&mut output_cards, Easy, easy_interval);
self.set_due(&mut output_cards, Easy, Duration::days(easy_interval));
}
Learning | Relearning => {
self.next_stability(&mut output_cards, card.state);
self.next_difficulty(&mut output_cards);

self.set_scheduled_days(&mut output_cards, Again, 0);
self.set_due(&mut output_cards, Again, Duration::minutes(5));

self.set_scheduled_days(&mut output_cards, Hard, 0);
self.set_due(&mut output_cards, Hard, Duration::minutes(10));

let good_interval = self.next_interval(&mut output_cards, Good).unwrap();

let easy_interval =
(good_interval + 1).max(self.next_interval(&mut output_cards, Easy).unwrap());

self.set_scheduled_days(&mut output_cards, Good, good_interval);
self.set_due(&mut output_cards, Good, Duration::days(good_interval));

self.set_scheduled_days(&mut output_cards, Easy, easy_interval);
self.set_due(&mut output_cards, Easy, Duration::days(easy_interval));
}
Review => {
self.next_stability(&mut output_cards, card.state);
self.next_difficulty(&mut output_cards);

let mut hard_interval = self.next_interval(&mut output_cards, Hard).unwrap();
let mut good_interval = self.next_interval(&mut output_cards, Good).unwrap();
let mut easy_interval = self.next_interval(&mut output_cards, Easy).unwrap();

hard_interval = cmp::min(hard_interval, good_interval);
good_interval = cmp::max(good_interval, hard_interval + 1);
easy_interval = cmp::max(good_interval + 1, easy_interval);

self.set_scheduled_days(&mut output_cards, Again, 0);
self.set_due(&mut output_cards, Again, Duration::minutes(5));

self.set_scheduled_days(&mut output_cards, Hard, hard_interval);
self.set_due(&mut output_cards, Hard, Duration::days(hard_interval));

self.set_scheduled_days(&mut output_cards, Good, good_interval);
self.set_due(&mut output_cards, Good, Duration::days(good_interval));

self.set_scheduled_days(&mut output_cards, Easy, easy_interval);
self.set_due(&mut output_cards, Easy, Duration::days(easy_interval));
}
pub fn scheduler(&self, card: Card, now: DateTime<Utc>) -> Box<dyn ImplScheduler> {
if self.parameters.enable_short_term {
Box::new(BasicScheduler::new(self.parameters, card, now))
} else {
Box::new(LongtermScheduler::new(self.parameters, card, now))
}
self.save_logs(&mut output_cards);
output_cards
}

fn set_due(&self, output_cards: &mut ScheduledCards, rating: Rating, duration: Duration) {
let Some(card) = output_cards.cards.get_mut(&rating) else {
return;
};
card.due = output_cards.now + duration;
pub fn repeat(&self, card: Card, now: DateTime<Utc>) -> RecordLog {
self.scheduler(card, now).preview()
}

fn set_scheduled_days(&self, output_cards: &mut ScheduledCards, rating: Rating, interval: i64) {
let Some(card) = output_cards.cards.get_mut(&rating) else {
return;
};
card.scheduled_days = interval;
}

fn save_logs(&self, output_cards: &mut ScheduledCards) {
for rating in Rating::iter() {
let Some(card) = output_cards.cards.get_mut(rating) else {
continue;
};
card.save_log(*rating);
}
}

fn init_difficulty_stability(&self, output_cards: &mut ScheduledCards) {
for rating in Rating::iter() {
let Some(card) = output_cards.cards.get_mut(rating) else {
continue;
};
card.difficulty = self.init_difficulty(*rating);
card.stability = self.init_stability(*rating);
}
}

fn init_difficulty(&self, rating: Rating) -> f32 {
let rating_int: i32 = rating as i32;

(self.params.w[4] - f32::exp(self.params.w[5] * (rating_int as f32 - 1.0)) + 1.0)
.clamp(1.0, 10.0)
}

fn init_stability(&self, rating: Rating) -> f32 {
let rating_int: i32 = rating as i32;
self.params.w[(rating_int - 1) as usize].max(0.1)
}

#[allow(clippy::suboptimal_flops)]
fn next_interval(
&self,
output_cards: &mut ScheduledCards,
rating: Rating,
) -> Result<i64, String> {
let Some(card) = output_cards.cards.get_mut(&rating) else {
return Err("Failed to retrieve card from output_cards".to_string());
};
let new_interval =
card.stability / FACTOR * (self.params.request_retention.powf(1.0 / DECAY) - 1.0);
Ok((new_interval.round() as i64).clamp(1, self.params.maximum_interval as i64))
}

fn next_stability(&self, output_cards: &mut ScheduledCards, state: State) {
if state == Learning || state == Relearning {
self.short_term_stability(output_cards)
} else if state == Review {
for rating in Rating::iter() {
if rating == &Again {
self.next_forget_stability(output_cards);
} else {
self.next_recall_stability(output_cards, *rating);
}
}
}
}

fn next_recall_stability(&self, output_cards: &mut ScheduledCards, rating: Rating) {
let modifier = match rating {
Hard => self.params.w[15],
Easy => self.params.w[16],
_ => 1.0,
};

let Some(card) = output_cards.cards.get_mut(&rating) else {
return;
};
let retrievability = card.get_retrievability();
card.stability = card.stability
* (((self.params.w[8]).exp()
* (11.0 - card.difficulty)
* card.stability.powf(-self.params.w[9])
* (((1.0 - retrievability) * self.params.w[10]).exp_m1()))
.mul_add(modifier, 1.0));
}

fn next_forget_stability(&self, output_cards: &mut ScheduledCards) {
let Some(card) = output_cards.cards.get_mut(&Again) else {
return;
};
let retrievability = card.get_retrievability();
card.stability = self.params.w[11]
* card.difficulty.powf(-self.params.w[12])
* ((card.stability + 1.0).powf(self.params.w[13]) - 1.0)
* f32::exp((1.0 - retrievability) * self.params.w[14])
}

fn next_difficulty(&self, output_cards: &mut ScheduledCards) {
for rating in Rating::iter() {
let rating_int = *rating as i32;
let Some(mut card) = output_cards.cards.remove(rating) else {
continue;
};
let next_difficulty =
self.params.w[6].mul_add(-(rating_int as f32 - 3.0), card.difficulty);
let mean_reversion = self.mean_reversion(self.init_difficulty(Easy), next_difficulty);
card.difficulty = mean_reversion.clamp(1.0, 10.0);
output_cards.cards.insert(*rating, card);
}
}

fn mean_reversion(&self, initial: f32, current: f32) -> f32 {
self.params.w[7].mul_add(initial, (1.0 - self.params.w[7]) * current)
}

fn short_term_stability(&self, output_cards: &mut ScheduledCards) {
for rating in Rating::iter() {
let rating_int = *rating as i32;
let Some(card) = output_cards.cards.get_mut(rating) else {
continue;
};
card.stability *=
f32::exp(self.params.w[17] * (rating_int as f32 - 3.0 + self.params.w[18]));
}
pub fn next(&self, card: Card, now: DateTime<Utc>, rating: Rating) -> SchedulingInfo {
self.scheduler(card, now).review(rating)
}
}
13 changes: 12 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
mod algo;
pub use algo::FSRS;

mod scheduler;
pub use scheduler::{ImplScheduler, Scheduler};

mod scheduler_basic;
pub use scheduler_basic::BasicScheduler;
mod scheduler_longterm;
pub use scheduler_longterm::LongtermScheduler;

mod models;
pub use models::{Card, Parameters, Rating, ReviewLog, ScheduledCards, State};
pub use models::{Card, Rating, ReviewLog, SchedulingInfo, State};

mod parameters;
pub use crate::parameters::Parameters;
mod tests;
Loading

0 comments on commit 2e329b6

Please sign in to comment.