mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
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:
parent
378c955905
commit
598233299e
7 changed files with 151 additions and 56 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -48,13 +48,27 @@ 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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
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,
|
||||||
|
),
|
||||||
ease_factor: ctx.initial_ease_factor,
|
ease_factor: ctx.initial_ease_factor,
|
||||||
memory_state,
|
memory_state,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -62,6 +76,7 @@ impl LearnState {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn answer_hard(self, ctx: &StateContext) -> CardState {
|
fn answer_hard(self, ctx: &StateContext) -> CardState {
|
||||||
let memory_state = ctx.fsrs_next_states.as_ref().map(|s| s.hard.memory.into());
|
let memory_state = ctx.fsrs_next_states.as_ref().map(|s| s.hard.memory.into());
|
||||||
|
@ -75,13 +90,27 @@ 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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if short_term {
|
||||||
|
LearnState {
|
||||||
|
scheduled_secs: (interval * 86_400.0) as u32,
|
||||||
|
elapsed_secs: 0,
|
||||||
|
memory_state,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
ReviewState {
|
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,
|
||||||
|
),
|
||||||
ease_factor: ctx.initial_ease_factor,
|
ease_factor: ctx.initial_ease_factor,
|
||||||
memory_state,
|
memory_state,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -89,6 +118,7 @@ impl LearnState {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn answer_good(self, ctx: &StateContext) -> CardState {
|
fn answer_good(self, ctx: &StateContext) -> CardState {
|
||||||
let memory_state = ctx.fsrs_next_states.as_ref().map(|s| s.good.memory.into());
|
let memory_state = ctx.fsrs_next_states.as_ref().map(|s| s.good.memory.into());
|
||||||
|
@ -102,13 +132,27 @@ 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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if short_term {
|
||||||
|
LearnState {
|
||||||
|
scheduled_secs: (interval * 86_400.0) as u32,
|
||||||
|
elapsed_secs: 0,
|
||||||
|
memory_state,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
ReviewState {
|
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,
|
||||||
|
),
|
||||||
ease_factor: ctx.initial_ease_factor,
|
ease_factor: ctx.initial_ease_factor,
|
||||||
memory_state,
|
memory_state,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -116,13 +160,14 @@ impl LearnState {
|
||||||
.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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue