From 6ff309e08f027e91fb3f065679ac3ef02057ba34 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Mon, 21 Oct 2024 13:09:07 +0800 Subject: [PATCH] Feat/option to enable FSRS short-term scheduler when (re)learning steps run out && speed up features based on simulation (#3505) * Update to FSRS-rs v1.3.2 * add fsrs_short_term_with_steps_enabled to config * ./ninja fix:minilints * fix defaults_for_testing * if current parameters are invalid, skip comparison fix #3498 * fix redundant_field_names * cargo clippy --fix * Update to FSRS-rs v1.3.3 * Update to FSRS-rs v1.3.4 * Avoid an extra config lookup on each card answer (dae) --- Cargo.lock | 16 ++++++++++++++-- Cargo.toml | 2 +- cargo/licenses.json | 11 ++++++++++- proto/anki/config.proto | 2 ++ pylib/anki/collection.py | 10 ++++++++++ rslib/src/backend/config.rs | 1 + rslib/src/config/bool.rs | 1 + rslib/src/preferences.rs | 7 ++++++- rslib/src/scheduler/answering/mod.rs | 5 +++++ rslib/src/scheduler/fsrs/weights.rs | 15 ++++++++------- rslib/src/scheduler/states/learning.rs | 9 ++++++--- rslib/src/scheduler/states/mod.rs | 2 ++ rslib/src/scheduler/states/relearning.rs | 12 +++++++++--- rslib/src/scheduler/states/review.rs | 4 +++- 14 files changed, 78 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38bcdd6f6..83ce00bfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1860,15 +1860,16 @@ dependencies = [ [[package]] name = "fsrs" -version = "1.3.1" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2434366942bf285f3c0691e68d731e56f4e1fc1d8ec7b6a0e9411e94eda6ffbd" +checksum = "a05b1fdbaa34f9bcd605ad50477eea51b27b3f60f973021bd9ee1b5943169150" dependencies = [ "burn", "itertools 0.12.1", "log", "ndarray", "ndarray-rand", + "priority-queue", "rand", "rayon", "serde", @@ -4276,6 +4277,17 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "priority-queue" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" +dependencies = [ + "autocfg", + "equivalent", + "indexmap", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" diff --git a/Cargo.toml b/Cargo.toml index 8d3c0052b..dc1d3a5f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] -version = "=1.3.1" +version = "=1.3.4" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # path = "../open-spaced-repetition/fsrs-rs" diff --git a/cargo/licenses.json b/cargo/licenses.json index 83bbcf653..9054526ad 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -1225,7 +1225,7 @@ }, { "name": "fsrs", - "version": "1.3.1", + "version": "1.3.4", "authors": "Open Spaced Repetition", "repository": "https://github.com/open-spaced-repetition/fsrs-rs", "license": "BSD-3-Clause", @@ -2753,6 +2753,15 @@ "license_file": null, "description": "A minimal `syn` syntax tree pretty-printer" }, + { + "name": "priority-queue", + "version": "2.1.1", + "authors": "Gianmarco Garrisi ", + "repository": "https://github.com/garro95/priority-queue", + "license": "LGPL-3.0-or-later OR MPL-2.0", + "license_file": null, + "description": "A Priority Queue implemented as a heap with a function to efficiently change the priority of an item." + }, { "name": "proc-macro-crate", "version": "3.2.0", diff --git a/proto/anki/config.proto b/proto/anki/config.proto index 9924ab90f..d61f139d6 100644 --- a/proto/anki/config.proto +++ b/proto/anki/config.proto @@ -55,6 +55,7 @@ message ConfigKey { SHIFT_POSITION_OF_EXISTING_CARDS = 24; RENDER_LATEX = 25; LOAD_BALANCER_ENABLED = 26; + FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27; } enum String { SET_DUE_BROWSER = 0; @@ -117,6 +118,7 @@ message Preferences { bool show_intervals_on_buttons = 4; uint32 time_limit_secs = 5; bool load_balancer_enabled = 6; + bool fsrs_short_term_with_steps_enabled = 7; } message Editing { bool adding_defaults_to_current_deck = 1; diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 559434cda..208ed6448 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -992,6 +992,16 @@ class Collection(DeprecatedNamesMixin): fget=_get_enable_load_balancer, fset=_set_enable_load_balancer ) + def _get_enable_fsrs_short_term_with_steps(self) -> bool: + return self.get_config_bool(Config.Bool.FSRS_SHORT_TERM_WITH_STEPS_ENABLED) + + def _set_enable_fsrs_short_term_with_steps(self, value: bool) -> None: + self.set_config_bool(Config.Bool.FSRS_SHORT_TERM_WITH_STEPS_ENABLED, value) + + fsrs_short_term_with_steps_enabled = property( + fget=_get_enable_fsrs_short_term_with_steps, + fset=_set_enable_fsrs_short_term_with_steps, + ) # Stats ########################################################################## diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 137124b45..6e0bed153 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -38,6 +38,7 @@ impl From for BoolKey { BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards, BoolKeyProto::RenderLatex => BoolKey::RenderLatex, BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled, + BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled, } } } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index c946f7c34..b430babe4 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -41,6 +41,7 @@ pub enum BoolKey { WithDeckConfigs, Fsrs, LoadBalancerEnabled, + FsrsShortTermWithStepsEnabled, #[strum(to_string = "normalize_note_text")] NormalizeNoteText, #[strum(to_string = "dayLearnFirst")] diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index e3b7e6f3c..96be8e461 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -99,6 +99,8 @@ impl Collection { .get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons), time_limit_secs: self.get_answer_time_limit_secs(), load_balancer_enabled: self.get_config_bool(BoolKey::LoadBalancerEnabled), + fsrs_short_term_with_steps_enabled: self + .get_config_bool(BoolKey::FsrsShortTermWithStepsEnabled), }) } @@ -119,7 +121,10 @@ impl Collection { )?; self.set_answer_time_limit_secs(s.time_limit_secs)?; self.set_config_bool_inner(BoolKey::LoadBalancerEnabled, s.load_balancer_enabled)?; - + self.set_config_bool_inner( + BoolKey::FsrsShortTermWithStepsEnabled, + s.fsrs_short_term_with_steps_enabled, + )?; Ok(()) } diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 5e85dce1f..169205b31 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -73,6 +73,7 @@ struct CardStateUpdater { fsrs_next_states: Option, /// Set if FSRS is enabled. desired_retention: Option, + fsrs_short_term_with_steps: bool, } impl CardStateUpdater { @@ -110,6 +111,7 @@ impl CardStateUpdater { Default::default() }, fsrs_next_states: self.fsrs_next_states.clone(), + fsrs_short_term_with_steps_enabled: self.fsrs_short_term_with_steps, } } @@ -458,6 +460,8 @@ impl Collection { None }; let desired_retention = fsrs_enabled.then_some(config.inner.desired_retention); + let fsrs_short_term_with_steps = + self.get_config_bool(BoolKey::FsrsShortTermWithStepsEnabled); Ok(CardStateUpdater { fuzz_seed: get_fuzz_seed(&card, false), card, @@ -467,6 +471,7 @@ impl Collection { now: TimestampSecs::now(), fsrs_next_states, desired_retention, + fsrs_short_term_with_steps, }) } diff --git a/rslib/src/scheduler/fsrs/weights.rs b/rslib/src/scheduler/fsrs/weights.rs index 3350487e4..5fbe59172 100644 --- a/rslib/src/scheduler/fsrs/weights.rs +++ b/rslib/src/scheduler/fsrs/weights.rs @@ -97,13 +97,14 @@ impl Collection { } } }); - let fsrs = FSRS::new(Some(current_weights))?; - let current_rmse = fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; - let mut weights = fsrs.compute_parameters(items.clone(), Some(progress2))?; - let optimized_fsrs = FSRS::new(Some(&weights))?; - let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; - if current_rmse <= optimized_rmse { - weights = current_weights.to_vec(); + let mut weights = FSRS::new(None)?.compute_parameters(items.clone(), Some(progress2))?; + if let Ok(fsrs) = FSRS::new(Some(current_weights)) { + let current_rmse = fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; + let optimized_fsrs = FSRS::new(Some(&weights))?; + let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; + if current_rmse <= optimized_rmse { + weights = current_weights.to_vec(); + } } Ok(ComputeFsrsWeightsResponse { diff --git a/rslib/src/scheduler/states/learning.rs b/rslib/src/scheduler/states/learning.rs index 57b610afc..0dc5782be 100644 --- a/rslib/src/scheduler/states/learning.rs +++ b/rslib/src/scheduler/states/learning.rs @@ -51,7 +51,8 @@ impl LearnState { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states { ( states.again.interval, - ctx.steps.is_empty() && states.again.interval < 0.5, + (ctx.fsrs_short_term_with_steps_enabled || ctx.steps.is_empty()) + && states.again.interval < 0.5, ) } else { (ctx.graduating_interval_good as f32, false) @@ -96,7 +97,8 @@ impl LearnState { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states { ( states.hard.interval, - ctx.steps.is_empty() && states.hard.interval < 0.5, + (ctx.fsrs_short_term_with_steps_enabled || ctx.steps.is_empty()) + && states.hard.interval < 0.5, ) } else { (ctx.graduating_interval_good as f32, false) @@ -141,7 +143,8 @@ impl LearnState { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states { ( states.good.interval, - ctx.steps.is_empty() && states.good.interval < 0.5, + (ctx.fsrs_short_term_with_steps_enabled || ctx.steps.is_empty()) + && states.good.interval < 0.5, ) } else { (ctx.graduating_interval_good as f32, false) diff --git a/rslib/src/scheduler/states/mod.rs b/rslib/src/scheduler/states/mod.rs index b721e1da2..527298a87 100644 --- a/rslib/src/scheduler/states/mod.rs +++ b/rslib/src/scheduler/states/mod.rs @@ -88,6 +88,7 @@ pub(crate) struct StateContext<'a> { /// range. pub fuzz_factor: Option, pub fsrs_next_states: Option, + pub fsrs_short_term_with_steps_enabled: bool, // learning pub steps: LearningSteps<'a>, @@ -147,6 +148,7 @@ impl<'a> StateContext<'a> { good: 0, }, fsrs_next_states: None, + fsrs_short_term_with_steps_enabled: false, } } } diff --git a/rslib/src/scheduler/states/relearning.rs b/rslib/src/scheduler/states/relearning.rs index 0659d18e2..31ffe5710 100644 --- a/rslib/src/scheduler/states/relearning.rs +++ b/rslib/src/scheduler/states/relearning.rs @@ -68,7 +68,9 @@ impl RelearnState { }, review: again_review, }; - if ctx.relearn_steps.is_empty() && interval < 0.5 { + if (ctx.fsrs_short_term_with_steps_enabled || ctx.relearn_steps.is_empty()) + && interval < 0.5 + { again_relearn.into() } else { again_review.into() @@ -112,7 +114,9 @@ impl RelearnState { }, review: hard_review, }; - if ctx.relearn_steps.is_empty() && interval < 0.5 { + if (ctx.fsrs_short_term_with_steps_enabled || ctx.relearn_steps.is_empty()) + && interval < 0.5 + { hard_relearn.into() } else { hard_review.into() @@ -162,7 +166,9 @@ impl RelearnState { }, review: good_review, }; - if ctx.relearn_steps.is_empty() && interval < 0.5 { + if (ctx.fsrs_short_term_with_steps_enabled || ctx.relearn_steps.is_empty()) + && interval < 0.5 + { good_relearn.into() } else { good_review.into() diff --git a/rslib/src/scheduler/states/review.rs b/rslib/src/scheduler/states/review.rs index 7f7a215f1..f173c2310 100644 --- a/rslib/src/scheduler/states/review.rs +++ b/rslib/src/scheduler/states/review.rs @@ -124,7 +124,9 @@ impl ReviewState { review: again_review, } .into() - } else if ctx.relearn_steps.is_empty() && scheduled_days < 0.5 { + } else if (ctx.fsrs_short_term_with_steps_enabled || ctx.relearn_steps.is_empty()) + && scheduled_days < 0.5 + { again_relearn.into() } else { again_review.into()