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)
This commit is contained in:
Jarrett Ye 2024-10-21 13:09:07 +08:00 committed by GitHub
parent b09326cddd
commit 6ff309e08f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 78 additions and 19 deletions

16
Cargo.lock generated
View file

@ -1860,15 +1860,16 @@ dependencies = [
[[package]] [[package]]
name = "fsrs" name = "fsrs"
version = "1.3.1" version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2434366942bf285f3c0691e68d731e56f4e1fc1d8ec7b6a0e9411e94eda6ffbd" checksum = "a05b1fdbaa34f9bcd605ad50477eea51b27b3f60f973021bd9ee1b5943169150"
dependencies = [ dependencies = [
"burn", "burn",
"itertools 0.12.1", "itertools 0.12.1",
"log", "log",
"ndarray", "ndarray",
"ndarray-rand", "ndarray-rand",
"priority-queue",
"rand", "rand",
"rayon", "rayon",
"serde", "serde",
@ -4276,6 +4277,17 @@ dependencies = [
"syn 2.0.79", "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]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.2.0" version = "3.2.0"

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.3.1" version = "=1.3.4"
# 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.3.1", "version": "1.3.4",
"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",
@ -2753,6 +2753,15 @@
"license_file": null, "license_file": null,
"description": "A minimal `syn` syntax tree pretty-printer" "description": "A minimal `syn` syntax tree pretty-printer"
}, },
{
"name": "priority-queue",
"version": "2.1.1",
"authors": "Gianmarco Garrisi <gianmarcogarrisi@tutanota.com>",
"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", "name": "proc-macro-crate",
"version": "3.2.0", "version": "3.2.0",

View file

@ -55,6 +55,7 @@ message ConfigKey {
SHIFT_POSITION_OF_EXISTING_CARDS = 24; SHIFT_POSITION_OF_EXISTING_CARDS = 24;
RENDER_LATEX = 25; RENDER_LATEX = 25;
LOAD_BALANCER_ENABLED = 26; LOAD_BALANCER_ENABLED = 26;
FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27;
} }
enum String { enum String {
SET_DUE_BROWSER = 0; SET_DUE_BROWSER = 0;
@ -117,6 +118,7 @@ message Preferences {
bool show_intervals_on_buttons = 4; bool show_intervals_on_buttons = 4;
uint32 time_limit_secs = 5; uint32 time_limit_secs = 5;
bool load_balancer_enabled = 6; bool load_balancer_enabled = 6;
bool fsrs_short_term_with_steps_enabled = 7;
} }
message Editing { message Editing {
bool adding_defaults_to_current_deck = 1; bool adding_defaults_to_current_deck = 1;

View file

@ -992,6 +992,16 @@ class Collection(DeprecatedNamesMixin):
fget=_get_enable_load_balancer, fset=_set_enable_load_balancer 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 # Stats
########################################################################## ##########################################################################

View file

@ -38,6 +38,7 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards, BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
BoolKeyProto::RenderLatex => BoolKey::RenderLatex, BoolKeyProto::RenderLatex => BoolKey::RenderLatex,
BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled, BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled,
BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled,
} }
} }
} }

View file

@ -41,6 +41,7 @@ pub enum BoolKey {
WithDeckConfigs, WithDeckConfigs,
Fsrs, Fsrs,
LoadBalancerEnabled, LoadBalancerEnabled,
FsrsShortTermWithStepsEnabled,
#[strum(to_string = "normalize_note_text")] #[strum(to_string = "normalize_note_text")]
NormalizeNoteText, NormalizeNoteText,
#[strum(to_string = "dayLearnFirst")] #[strum(to_string = "dayLearnFirst")]

View file

@ -99,6 +99,8 @@ impl Collection {
.get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons), .get_config_bool(BoolKey::ShowIntervalsAboveAnswerButtons),
time_limit_secs: self.get_answer_time_limit_secs(), time_limit_secs: self.get_answer_time_limit_secs(),
load_balancer_enabled: self.get_config_bool(BoolKey::LoadBalancerEnabled), 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_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::LoadBalancerEnabled, s.load_balancer_enabled)?;
self.set_config_bool_inner(
BoolKey::FsrsShortTermWithStepsEnabled,
s.fsrs_short_term_with_steps_enabled,
)?;
Ok(()) Ok(())
} }

View file

@ -73,6 +73,7 @@ struct CardStateUpdater {
fsrs_next_states: Option<NextStates>, fsrs_next_states: Option<NextStates>,
/// Set if FSRS is enabled. /// Set if FSRS is enabled.
desired_retention: Option<f32>, desired_retention: Option<f32>,
fsrs_short_term_with_steps: bool,
} }
impl CardStateUpdater { impl CardStateUpdater {
@ -110,6 +111,7 @@ impl CardStateUpdater {
Default::default() Default::default()
}, },
fsrs_next_states: self.fsrs_next_states.clone(), 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 None
}; };
let desired_retention = fsrs_enabled.then_some(config.inner.desired_retention); 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 { Ok(CardStateUpdater {
fuzz_seed: get_fuzz_seed(&card, false), fuzz_seed: get_fuzz_seed(&card, false),
card, card,
@ -467,6 +471,7 @@ impl Collection {
now: TimestampSecs::now(), now: TimestampSecs::now(),
fsrs_next_states, fsrs_next_states,
desired_retention, desired_retention,
fsrs_short_term_with_steps,
}) })
} }

View file

@ -97,14 +97,15 @@ impl Collection {
} }
} }
}); });
let fsrs = FSRS::new(Some(current_weights))?; 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 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_fsrs = FSRS::new(Some(&weights))?;
let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
if current_rmse <= optimized_rmse { if current_rmse <= optimized_rmse {
weights = current_weights.to_vec(); weights = current_weights.to_vec();
} }
}
Ok(ComputeFsrsWeightsResponse { Ok(ComputeFsrsWeightsResponse {
weights, weights,

View file

@ -51,7 +51,8 @@ impl LearnState {
let (interval, short_term) = 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,
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 { } else {
(ctx.graduating_interval_good as f32, false) (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 { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
( (
states.hard.interval, 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 { } else {
(ctx.graduating_interval_good as f32, false) (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 { let (interval, short_term) = if let Some(states) = &ctx.fsrs_next_states {
( (
states.good.interval, 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 { } else {
(ctx.graduating_interval_good as f32, false) (ctx.graduating_interval_good as f32, false)

View file

@ -88,6 +88,7 @@ pub(crate) struct StateContext<'a> {
/// range. /// range.
pub fuzz_factor: Option<f32>, pub fuzz_factor: Option<f32>,
pub fsrs_next_states: Option<NextStates>, pub fsrs_next_states: Option<NextStates>,
pub fsrs_short_term_with_steps_enabled: bool,
// learning // learning
pub steps: LearningSteps<'a>, pub steps: LearningSteps<'a>,
@ -147,6 +148,7 @@ impl<'a> StateContext<'a> {
good: 0, good: 0,
}, },
fsrs_next_states: None, fsrs_next_states: None,
fsrs_short_term_with_steps_enabled: false,
} }
} }
} }

View file

@ -68,7 +68,9 @@ impl RelearnState {
}, },
review: again_review, 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() again_relearn.into()
} else { } else {
again_review.into() again_review.into()
@ -112,7 +114,9 @@ impl RelearnState {
}, },
review: hard_review, 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() hard_relearn.into()
} else { } else {
hard_review.into() hard_review.into()
@ -162,7 +166,9 @@ impl RelearnState {
}, },
review: good_review, 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() good_relearn.into()
} else { } else {
good_review.into() good_review.into()

View file

@ -124,7 +124,9 @@ impl ReviewState {
review: again_review, review: again_review,
} }
.into() .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() again_relearn.into()
} else { } else {
again_review.into() again_review.into()