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]]
name = "fsrs"
version = "1.2.4"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c3e9d1ab337e63735c4ceff913b9818eeac054d8dc17ca1b259195a7fbfb16"
checksum = "2434366942bf285f3c0691e68d731e56f4e1fc1d8ec7b6a0e9411e94eda6ffbd"
dependencies = [
"burn",
"itertools 0.12.1",

View file

@ -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"

View file

@ -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",

View file

@ -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,

View file

@ -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()
}
}
@ -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()
}
}
@ -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
};

View file

@ -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
@ -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()
}
@ -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()
}
@ -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()
}
@ -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
};

View file

@ -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
@ -87,7 +87,7 @@ impl ReviewState {
minimum,
maximum,
);
(interval, None)
(interval as f32, None)
}
}
@ -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 {
@ -115,6 +124,8 @@ impl ReviewState {
review: again_review,
}
.into()
} else if scheduled_days < 0.5 {
again_relearn.into()
} else {
again_review.into()
}
@ -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)