Let FSRS control short term schedule (#3375)

* 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)

https://github.com/ankidroid/Anki-Android-Backend/pull/417

* Remove redundant parens (dae)
This commit is contained in:
Jarrett Ye 2024-10-06 10:20:18 +08:00 committed by GitHub
parent 378c955905
commit 598233299e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 151 additions and 56 deletions

4
Cargo.lock generated
View file

@ -1862,9 +1862,9 @@ dependencies = [
[[package]] [[package]]
name = "fsrs" name = "fsrs"
version = "1.2.4" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c3e9d1ab337e63735c4ceff913b9818eeac054d8dc17ca1b259195a7fbfb16" checksum = "2434366942bf285f3c0691e68d731e56f4e1fc1d8ec7b6a0e9411e94eda6ffbd"
dependencies = [ dependencies = [
"burn", "burn",
"itertools 0.12.1", "itertools 0.12.1",

View file

@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs] [workspace.dependencies.fsrs]
version = "1.2.4" version = "=1.3.1"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
# path = "../open-spaced-repetition/fsrs-rs" # path = "../open-spaced-repetition/fsrs-rs"

View file

@ -1225,7 +1225,7 @@
}, },
{ {
"name": "fsrs", "name": "fsrs",
"version": "1.2.4", "version": "1.3.1",
"authors": "Open Spaced Repetition", "authors": "Open Spaced Repetition",
"repository": "https://github.com/open-spaced-repetition/fsrs-rs", "repository": "https://github.com/open-spaced-repetition/fsrs-rs",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",

View file

@ -105,8 +105,7 @@ impl Collection {
Some(state.stability), Some(state.stability),
card.desired_retention.unwrap(), card.desired_retention.unwrap(),
0, 0,
) );
as f32;
card.interval = with_review_fuzz( card.interval = with_review_fuzz(
card.get_fuzz_factor(true), card.get_fuzz_factor(true),
interval, interval,

View file

@ -48,18 +48,33 @@ impl LearnState {
.into() .into()
} else { } else {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
states.again.interval (states.again.interval, states.again.interval < 0.5)
} else { } else {
ctx.graduating_interval_good (ctx.graduating_interval_good as f32, false)
}; };
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), if short_term {
ease_factor: ctx.initial_ease_factor, LearnState {
memory_state, remaining_steps: ctx.steps.remaining_for_failed(),
..Default::default() 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()
} }
} }
@ -75,18 +90,33 @@ impl LearnState {
.into() .into()
} else { } else {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
states.hard.interval (states.hard.interval, states.hard.interval < 0.5)
} else { } else {
ctx.graduating_interval_good (ctx.graduating_interval_good as f32, false)
}; };
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), if short_term {
ease_factor: ctx.initial_ease_factor, LearnState {
memory_state, scheduled_secs: (interval * 86_400.0) as u32,
..Default::default() 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()
} }
} }
@ -102,27 +132,42 @@ impl LearnState {
.into() .into()
} else { } else {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
states.good.interval (states.good.interval, states.good.interval < 0.5)
} else { } else {
ctx.graduating_interval_good (ctx.graduating_interval_good as f32, false)
}; };
ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), if short_term {
ease_factor: ctx.initial_ease_factor, LearnState {
memory_state, scheduled_secs: (interval * 86_400.0) as u32,
..Default::default() 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 { fn answer_easy(self, ctx: &StateContext) -> ReviewState {
let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1); let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = if let Some(states) = &ctx.fsrs_next_states { 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; minimum = good + 1;
states.easy.interval states.easy.interval.round().max(1.0) as u32
} else { } else {
ctx.graduating_interval_easy ctx.graduating_interval_easy
}; };

View file

@ -45,7 +45,7 @@ impl RelearnState {
memory_state, memory_state,
}, },
review: ReviewState { review: ReviewState {
scheduled_days, scheduled_days: scheduled_days.round().max(1.0) as u32,
elapsed_days: 0, elapsed_days: 0,
memory_state, memory_state,
..self.review ..self.review
@ -55,11 +55,24 @@ impl RelearnState {
} else if let Some(states) = &ctx.fsrs_next_states { } else if let Some(states) = &ctx.fsrs_next_states {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = states.again.interval; let interval = states.again.interval;
ReviewState { let again_review = ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
..self.review ..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 { } else {
self.review.into() self.review.into()
} }
@ -87,11 +100,23 @@ impl RelearnState {
} else if let Some(states) = &ctx.fsrs_next_states { } else if let Some(states) = &ctx.fsrs_next_states {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = states.hard.interval; let interval = states.hard.interval;
ReviewState { let hard_review = ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
..self.review ..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 { } else {
self.review.into() self.review.into()
} }
@ -122,11 +147,26 @@ impl RelearnState {
} else if let Some(states) = &ctx.fsrs_next_states { } else if let Some(states) = &ctx.fsrs_next_states {
let (minimum, maximum) = ctx.min_and_max_review_intervals(1); let (minimum, maximum) = ctx.min_and_max_review_intervals(1);
let interval = states.good.interval; let interval = states.good.interval;
ReviewState { let good_review = ReviewState {
scheduled_days: ctx.with_review_fuzz(interval as f32, minimum, maximum), scheduled_days: ctx.with_review_fuzz(interval.round().max(1.0), minimum, maximum),
..self.review ..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 { } else {
self.review.into() self.review.into()
} }
@ -135,10 +175,10 @@ impl RelearnState {
fn answer_easy(self, ctx: &StateContext) -> ReviewState { fn answer_easy(self, ctx: &StateContext) -> ReviewState {
let scheduled_days = if let Some(states) = &ctx.fsrs_next_states { let scheduled_days = if let Some(states) = &ctx.fsrs_next_states {
let (mut minimum, maximum) = ctx.min_and_max_review_intervals(1); 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; minimum = good + 1;
let interval = states.easy.interval; 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 { } else {
self.review.scheduled_days + 1 self.review.scheduled_days + 1
}; };

View file

@ -75,7 +75,7 @@ impl ReviewState {
pub(crate) fn failing_review_interval( pub(crate) fn failing_review_interval(
self, self,
ctx: &StateContext, ctx: &StateContext,
) -> (u32, Option<FsrsMemoryState>) { ) -> (f32, Option<FsrsMemoryState>) {
if let Some(states) = &ctx.fsrs_next_states { if let Some(states) = &ctx.fsrs_next_states {
// In FSRS, fuzz is applied when the card leaves the relearning // In FSRS, fuzz is applied when the card leaves the relearning
// stage // stage
@ -87,7 +87,7 @@ impl ReviewState {
minimum, minimum,
maximum, maximum,
); );
(interval, None) (interval as f32, None)
} }
} }
@ -96,13 +96,22 @@ impl ReviewState {
let leeched = leech_threshold_met(lapses, ctx.leech_threshold); let leeched = leech_threshold_met(lapses, ctx.leech_threshold);
let (scheduled_days, memory_state) = self.failing_review_interval(ctx); let (scheduled_days, memory_state) = self.failing_review_interval(ctx);
let again_review = ReviewState { let again_review = ReviewState {
scheduled_days, scheduled_days: scheduled_days.round().max(1.0) as u32,
elapsed_days: 0, elapsed_days: 0,
ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR), ease_factor: (self.ease_factor + EASE_FACTOR_AGAIN_DELTA).max(MINIMUM_EASE_FACTOR),
lapses, lapses,
leeched, leeched,
memory_state, 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() { if let Some(again_delay) = ctx.relearn_steps.again_delay_secs_learn() {
RelearnState { RelearnState {
@ -115,6 +124,8 @@ impl ReviewState {
review: again_review, review: again_review,
} }
.into() .into()
} else if scheduled_days < 0.5 {
again_relearn.into()
} else { } else {
again_review.into() again_review.into()
} }
@ -177,20 +188,20 @@ impl ReviewState {
}; };
let hard = constrain_passing_interval( let hard = constrain_passing_interval(
ctx, ctx,
states.hard.interval as f32, states.hard.interval,
greater_than_last(states.hard.interval).max(1), greater_than_last(states.hard.interval.round() as u32).max(1),
true, true,
); );
let good = constrain_passing_interval( let good = constrain_passing_interval(
ctx, ctx,
states.good.interval as f32, states.good.interval,
greater_than_last(states.good.interval).max(hard + 1), greater_than_last(states.good.interval.round() as u32).max(hard + 1),
true, true,
); );
let easy = constrain_passing_interval( let easy = constrain_passing_interval(
ctx, ctx,
states.easy.interval as f32, states.easy.interval,
greater_than_last(states.easy.interval).max(good + 1), greater_than_last(states.easy.interval.round() as u32).max(good + 1),
true, true,
); );
(hard, good, easy) (hard, good, easy)