Skip to content

Commit

Permalink
Let FSRS control short term schedule (#3375)
Browse files Browse the repository at this point in the history
* graduate card when user press hard and has 0 learning steps

* fix error: useless conversion to the same type

* do the same thing to again

* fix expected `Option<u32>`, found integer

* ./ninja format

* let FSRS control short term schedule

* Update to FSRS-rs v1.3.0

* ./ninja check:clippy

* Update to FSRS-rs v1.3.1

* Pin FSRS version (dae)

ankidroid/Anki-Android-Backend#417

* Remove redundant parens (dae)
  • Loading branch information
L-M-Sherlock authored Oct 6, 2024
1 parent 378c955 commit 5982332
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 56 deletions.
4 changes: 2 additions & 2 deletions 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
Expand Up @@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"

[workspace.dependencies.fsrs]
version = "1.2.4"
version = "=1.3.1"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
# path = "../open-spaced-repetition/fsrs-rs"
Expand Down
2 changes: 1 addition & 1 deletion cargo/licenses.json
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,7 @@
},
{
"name": "fsrs",
"version": "1.2.4",
"version": "1.3.1",
"authors": "Open Spaced Repetition",
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
"license": "BSD-3-Clause",
Expand Down
3 changes: 1 addition & 2 deletions rslib/src/scheduler/fsrs/memory_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ impl Collection {
Some(state.stability),
card.desired_retention.unwrap(),
0,
)
as f32;
);
card.interval = with_review_fuzz(
card.get_fuzz_factor(true),
interval,
Expand Down
103 changes: 74 additions & 29 deletions rslib/src/scheduler/states/learning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,33 @@ impl LearnState {
.into()
} else {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states {
states.again.interval
let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
(states.again.interval, states.again.interval < 0.5)
} else {
ctx.graduating_interval_good
(ctx.graduating_interval_good as f32, false)
};
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
ease_factor: ctx.initial_ease_factor,
memory_state,
..Default::default()

if short_term {
LearnState {
remaining_steps: ctx.steps.remaining_for_failed(),
scheduled_secs: (interval * 86_400.0) as u32,
elapsed_secs: 0,
memory_state,
}
.into()
} else {
ReviewState {
scheduled_days: ctx.with_review_fuzz(
interval.round().max(1.0),
minimum,
maximum,
),
ease_factor: ctx.initial_ease_factor,
memory_state,
..Default::default()
}
.into()
}
.into()
}
}

Expand All @@ -75,18 +90,33 @@ impl LearnState {
.into()
} else {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states {
states.hard.interval
let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
(states.hard.interval, states.hard.interval < 0.5)
} else {
ctx.graduating_interval_good
(ctx.graduating_interval_good as f32, false)
};
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
ease_factor: ctx.initial_ease_factor,
memory_state,
..Default::default()

if short_term {
LearnState {
scheduled_secs: (interval * 86_400.0) as u32,
elapsed_secs: 0,
memory_state,
..self
}
.into()
} else {
ReviewState {
scheduled_days: ctx.with_review_fuzz(
interval.round().max(1.0),
minimum,
maximum,
),
ease_factor: ctx.initial_ease_factor,
memory_state,
..Default::default()
}
.into()
}
.into()
}
}

Expand All @@ -102,27 +132,42 @@ impl LearnState {
.into()
} else {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states {
states.good.interval
let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
(states.good.interval, states.good.interval < 0.5)
} else {
ctx.graduating_interval_good
(ctx.graduating_interval_good as f32, false)
};
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
ease_factor: ctx.initial_ease_factor,
memory_state,
..Default::default()

if short_term {
LearnState {
scheduled_secs: (interval * 86_400.0) as u32,
elapsed_secs: 0,
memory_state,
..self
}
.into()
} else {
ReviewState {
scheduled_days: ctx.with_review_fuzz(
interval.round().max(1.0),
minimum,
maximum,
),
ease_factor: ctx.initial_ease_factor,
memory_state,
..Default::default()
}
.into()
}
.into()
}
}

fn answer_easy(self, ctx: &StateContext) -> ReviewState {
let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states {
let good = ctx.with_review_fuzz(states.good.interval as f32, minimum, maximum);
let good = ctx.with_review_fuzz(states.good.interval, minimum, maximum);
minimum = good + 1;
states.easy.interval
states.easy.interval.round().max(1.0) as u32
} else {
ctx.graduating_interval_easy
};
Expand Down
64 changes: 52 additions & 12 deletions rslib/src/scheduler/states/relearning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl RelearnState {
memory_state,
},
review: ReviewState {
scheduled_days,
scheduled_days: scheduled_days.round().max(1.0) as u32,
elapsed_days: 0,
memory_state,
..self.review
Expand All @@ -55,11 +55,24 @@ impl RelearnState {
} else if let Some(states) = &ctx.fsrs_next_states {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = states.again.interval;
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
let again_review = ReviewState {
scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
..self.review
};
let again_relearn = RelearnState {
learning: LearnState {
remaining_steps: ctx.relearn_steps.remaining_for_failed(),
scheduled_secs: (interval * 86_400.0) as u32,
elapsed_secs: 0,
memory_state,
},
review: again_review,
};
if interval > 0.5 {
again_review.into()
} else {
again_relearn.into()
}
.into()
} else {
self.review.into()
}
Expand Down Expand Up @@ -87,11 +100,23 @@ impl RelearnState {
} else if let Some(states) = &ctx.fsrs_next_states {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = states.hard.interval;
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
let hard_review = ReviewState {
scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
..self.review
};
let hard_relearn = RelearnState {
learning: LearnState {
scheduled_secs: (interval * 86_400.0) as u32,
memory_state,
..self.learning
},
review: hard_review,
};
if interval > 0.5 {
hard_review.into()
} else {
hard_relearn.into()
}
.into()
} else {
self.review.into()
}
Expand Down Expand Up @@ -122,11 +147,26 @@ impl RelearnState {
} else if let Some(states) = &ctx.fsrs_next_states {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = states.good.interval;
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum),
let good_review = ReviewState {
scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
..self.review
};
let good_relearn = RelearnState {
learning: LearnState {
scheduled_secs: (interval * 86_400.0) as u32,
remaining_steps: ctx
.relearn_steps
.remaining_for_good(self.learning.remaining_steps),
memory_state,
..self.learning
},
review: good_review,
};
if interval > 0.5 {
good_review.into()
} else {
good_relearn.into()
}
.into()
} else {
self.review.into()
}
Expand All @@ -135,10 +175,10 @@ impl RelearnState {
fn answer_easy(self, ctx: &StateContext) -> ReviewState {
let scheduled_days = if let Some(states) = &ctx.fsrs_next_states {
let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1);
let good = ctx.with_review_fuzz(states.good.interval as f32, minimum, maximum);
let good = ctx.with_review_fuzz(states.good.interval, minimum, maximum);
minimum = good + 1;
let interval = states.easy.interval;
ctx.with_review_fuzz(interval as f32, minimum, maximum)
ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum)
} else {
self.review.scheduled_days + 1
};
Expand Down
29 changes: 20 additions & 9 deletions rslib/src/scheduler/states/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl ReviewState {
pub(crate) fn failing_review_interval(
self,
ctx: &StateContext,
) -> (u32, Option<FsrsMemoryState>) {
) -> (f32, Option<FsrsMemoryState>) {
if let Some(states) = &ctx.fsrs_next_states {
// In FSRS, fuzz is applied when the card leaves the relearning
// stage
Expand All @@ -87,7 +87,7 @@ impl ReviewState {
minimum,
maximum,
);
(interval, None)
(interval as f32, None)
}
}

Expand All @@ -96,13 +96,22 @@ impl ReviewState {
let leeched = leech_threshold_met(lapses, ctx.leech_threshold);
let (scheduled_days, memory_state) = self.failing_review_interval(ctx);
let again_review = ReviewState {
scheduled_days,
scheduled_days: scheduled_days.round().max(1.0) as u32,
elapsed_days: 0,
ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR),
lapses,
leeched,
memory_state,
};
let again_relearn = RelearnState {
learning: LearnState {
remaining_steps: ctx.relearn_steps.remaining_for_failed(),
scheduled_secs: (scheduled_days * 86_400.0) as u32,
elapsed_secs: 0,
memory_state,
},
review: again_review,
};

if let Some(again_delay) = ctx.relearn_steps.again_delay_secs_learn() {
RelearnState {
Expand All @@ -115,6 +124,8 @@ impl ReviewState {
review: again_review,
}
.into()
} else if scheduled_days < 0.5 {
again_relearn.into()
} else {
again_review.into()
}
Expand Down Expand Up @@ -177,20 +188,20 @@ impl ReviewState {
};
let hard = constrain_passing_interval(
ctx,
states.hard.interval as f32,
greater_than_last(states.hard.interval).max(1),
states.hard.interval,
greater_than_last(states.hard.interval.round() as u32).max(1),
true,
);
let good = constrain_passing_interval(
ctx,
states.good.interval as f32,
greater_than_last(states.good.interval).max(hard + 1),
states.good.interval,
greater_than_last(states.good.interval.round() as u32).max(hard + 1),
true,
);
let easy = constrain_passing_interval(
ctx,
states.easy.interval as f32,
greater_than_last(states.easy.interval).max(good + 1),
states.easy.interval,
greater_than_last(states.easy.interval.round() as u32).max(good + 1),
true,
);
(hard, good, easy)
Expand Down

0 comments on commit 5982332

Please sign in to comment.