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

View file

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

View file

@ -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 <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",
"version": "3.2.0",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -73,6 +73,7 @@ struct CardStateUpdater {
fsrs_next_states: Option<NextStates>,
/// Set if FSRS is enabled.
desired_retention: Option<f32>,
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,
})
}

View file

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

View file

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

View file

@ -88,6 +88,7 @@ pub(crate) struct StateContext<'a> {
/// range.
pub fuzz_factor: Option<f32>,
pub fsrs_next_states: Option<NextStates>,
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,
}
}
}

View file

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

View file

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