diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index 179e98b3e..1ac48c969 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -107,9 +107,11 @@ message DeckConfig { repeated float learn_steps = 1; repeated float relearn_steps = 2; - repeated float fsrs_weights = 3; + repeated float fsrs_params_4 = 3; + repeated float fsrs_params_5 = 5; - reserved 5 to 8; + // consider saving remaining ones for fsrs param changes + reserved 6 to 8; uint32 new_per_day = 9; uint32 reviews_per_day = 10; diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index 489537d42..ea299da7b 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -74,7 +74,8 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner { bury_new: false, bury_reviews: false, bury_interday_learning: false, - fsrs_weights: vec![], + fsrs_params_4: vec![], + fsrs_params_5: vec![], desired_retention: 0.9, other: Vec::new(), historical_retention: 0.9, @@ -105,6 +106,15 @@ impl DeckConfig { self.mtime_secs = TimestampSecs::now(); self.usn = usn; } + + /// Retrieve the FSRS 5.0 params, falling back on 4.x ones. + pub fn fsrs_params(&self) -> &Vec { + if self.inner.fsrs_params_5.len() == 19 { + &self.inner.fsrs_params_5 + } else { + &self.inner.fsrs_params_4 + } + } } impl Collection { diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index c03c62d14..bde3f2234 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -69,8 +69,10 @@ pub struct DeckConfSchema11 { #[serde(default)] bury_interday_learning: bool, + #[serde(default, rename = "fsrsWeights")] + fsrs_params_4: Vec, #[serde(default)] - fsrs_weights: Vec, + fsrs_params_5: Vec, #[serde(default)] desired_retention: f32, #[serde(default)] @@ -306,7 +308,8 @@ impl Default for DeckConfSchema11 { new_sort_order: 0, new_gather_priority: 0, bury_interday_learning: false, - fsrs_weights: vec![], + fsrs_params_4: vec![], + fsrs_params_5: vec![], desired_retention: 0.9, sm2_retention: 0.9, weight_search: "".to_string(), @@ -386,7 +389,8 @@ impl From for DeckConfig { bury_new: c.new.bury, bury_reviews: c.rev.bury, bury_interday_learning: c.bury_interday_learning, - fsrs_weights: c.fsrs_weights, + fsrs_params_4: c.fsrs_params_4, + fsrs_params_5: c.fsrs_params_5, ignore_revlogs_before_date: c.ignore_revlogs_before_date, easy_days_percentages: c.easy_days_percentages, desired_retention: c.desired_retention, @@ -498,7 +502,8 @@ impl From for DeckConfSchema11 { new_sort_order: i.new_card_sort_order, new_gather_priority: i.new_card_gather_priority, bury_interday_learning: i.bury_interday_learning, - fsrs_weights: i.fsrs_weights, + fsrs_params_4: i.fsrs_params_4, + fsrs_params_5: i.fsrs_params_5, desired_retention: i.desired_retention, sm2_retention: i.historical_retention, weight_search: i.weight_search, @@ -526,6 +531,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! { "interdayLearningMix", "newGatherPriority", "fsrsWeights", + "fsrsParams5", "desiredRetention", "stopTimerOnAnswer", "secondsToShowQuestion", diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index dc68b8a39..e931dad4a 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -50,7 +50,7 @@ impl Collection { deck: DeckId, ) -> Result { let mut defaults = DeckConfig::default(); - defaults.inner.fsrs_weights = DEFAULT_PARAMETERS.into(); + defaults.inner.fsrs_params_5 = DEFAULT_PARAMETERS.into(); let last_optimize = self.get_config_i32(I32ConfigKey::LastFsrsOptimize) as u32; let days_since_last_fsrs_optimize = if last_optimize > 0 { self.timing_today()? @@ -88,6 +88,12 @@ impl Collection { // grab the config and sort it let mut config = self.storage.all_deck_config()?; config.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + // pre-fill empty fsrs 5 params with 4 params + config.iter_mut().for_each(|c| { + if c.inner.fsrs_params_5.is_empty() { + c.inner.fsrs_params_5 = c.inner.fsrs_params_4.clone(); + } + }); // combine with use counts let counts = self.get_deck_config_use_counts()?; @@ -159,8 +165,14 @@ impl Collection { // add/update provided configs for conf in &mut req.configs { + // If the user has provided empty FSRS5 params, zero out any + // old params as well, so we don't fall back on them, which would + // be surprising as they're not shown in the GUI. + if conf.inner.fsrs_params_5.is_empty() { + conf.inner.fsrs_params_4.clear(); + } // check the provided parameters are valid before we save them - FSRS::new(Some(&conf.inner.fsrs_weights))?; + FSRS::new(Some(conf.fsrs_params()))?; self.add_or_update_deck_config(conf)?; configs_after_update.insert(conf.id, conf.clone()); } @@ -201,7 +213,7 @@ impl Collection { let previous_order = previous_config .map(|c| c.inner.new_card_insert_order()) .unwrap_or_default(); - let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights); + let previous_weights = previous_config.map(|c| c.fsrs_params()); let previous_retention = previous_config.map(|c| c.inner.desired_retention); // if a selected (sub)deck, or its old config was removed, update deck to point @@ -228,7 +240,7 @@ impl Collection { } // if weights differ, memory state needs to be recomputed - let current_weights = current_config.map(|c| &c.inner.fsrs_weights); + let current_weights = current_config.map(|c| c.fsrs_params()); let current_retention = current_config.map(|c| c.inner.desired_retention); if fsrs_toggled || previous_weights != current_weights @@ -252,7 +264,7 @@ impl Collection { let weights = config.and_then(|c| { if req.fsrs { Some(UpdateMemoryStateRequest { - weights: c.inner.fsrs_weights.clone(), + weights: c.fsrs_params().clone(), desired_retention: c.inner.desired_retention, max_interval: c.inner.maximum_review_interval, reschedule: req.fsrs_reschedule, @@ -349,11 +361,11 @@ impl Collection { ignore_revlogs_before_ms, idx as u32 + 1, config_len, - &config.inner.fsrs_weights, + config.fsrs_params(), ) { Ok(weights) => { println!("{}: {:?}", config.name, weights.weights); - config.inner.fsrs_weights = weights.weights; + config.inner.fsrs_params_5 = weights.weights; } Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted), Err(err) => { diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 169205b31..9458e7559 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -431,7 +431,7 @@ impl Collection { let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs); let fsrs_next_states = if fsrs_enabled { - let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; + let fsrs = FSRS::new(Some(config.fsrs_params()))?; if card.memory_state.is_none() && card.ctype != CardType::New { // Card has been moved or imported into an FSRS deck after weights were set, // and will need its initial memory state to be calculated based on review diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 8dec27ba8..d6836986d 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -154,7 +154,7 @@ impl Collection { .or_not_found(conf_id)?; let desired_retention = config.inner.desired_retention; let historical_retention = config.inner.historical_retention; - let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; + let fsrs = FSRS::new(Some(config.fsrs_params()))?; let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?; let item = single_card_revlog_to_item( &fsrs, diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index b5da86d2b..ad713c813 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -130,7 +130,7 @@ impl Collection { .get_deck_config(conf_id)? .or_not_found(conf_id)?; let historical_retention = config.inner.historical_retention; - let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; + let fsrs = FSRS::new(Some(config.fsrs_params()))?; let next_day_at = self.timing_today()?.next_day_at; let ignore_before = ignore_revlogs_before_ms_from_config(&config)?; diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 2cef1537f..aa4d0aae6 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -26,7 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import SwitchRow from "$lib/components/SwitchRow.svelte"; import GlobalLabel from "./GlobalLabel.svelte"; - import type { DeckOptionsState } from "./lib"; + import { fsrsParams, type DeckOptionsState } from "./lib"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; import SpinBoxRow from "./SpinBoxRow.svelte"; import Warning from "./Warning.svelte"; @@ -82,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } const simulateFsrsRequest = new SimulateFsrsReviewRequest({ - weights: $config.fsrsWeights, + weights: fsrsParams($config), desiredRetention: $config.desiredRetention, deckSize: 0, daysToSimulate: 365, @@ -137,26 +137,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html try { await runWithBackendProgress( async () => { + const params = fsrsParams($config); const resp = await computeFsrsWeights({ search: $config.weightSearch ? $config.weightSearch : defaultWeightSearch, ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), - currentWeights: $config.fsrsWeights, + currentWeights: params, }); if ( - ($config.fsrsWeights.length && - $config.fsrsWeights.every( + (params.length && + params.every( (n, i) => n.toFixed(4) === resp.weights[i].toFixed(4), )) || resp.weights.length === 0 ) { setTimeout(() => alert(tr.deckConfigFsrsParamsOptimal()), 100); + } else { + $config.fsrsParams5 = resp.weights; } if (computeWeightsProgress) { computeWeightsProgress.current = computeWeightsProgress.total; } - $config.fsrsWeights = resp.weights; }, (progress) => { if (progress.value.case === "computeWeights") { @@ -187,7 +189,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ? $config.weightSearch : defaultWeightSearch; const resp = await evaluateWeights({ - weights: $config.fsrsWeights, + weights: fsrsParams($config), search, ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), }); @@ -230,7 +232,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html await runWithBackendProgress( async () => { optimalRetentionRequest.maxInterval = $config.maximumReviewInterval; - optimalRetentionRequest.weights = $config.fsrsWeights; + optimalRetentionRequest.weights = fsrsParams($config); optimalRetentionRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`; const resp = await computeOptimalRetention(optimalRetentionRequest); optimalRetention = resp.optimalRetention; @@ -311,7 +313,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html try { await runWithBackendProgress( async () => { - simulateFsrsRequest.weights = $config.fsrsWeights; + simulateFsrsRequest.weights = fsrsParams($config); simulateFsrsRequest.desiredRetention = $config.desiredRetention; simulateFsrsRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`; simulateProgressString = "processing..."; @@ -360,9 +362,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
openHelpModal("modelWeights")}> {tr.deckConfigWeights()} diff --git a/ts/routes/deck-options/lib.ts b/ts/routes/deck-options/lib.ts index 0c57f2977..dce872bd9 100644 --- a/ts/routes/deck-options/lib.ts +++ b/ts/routes/deck-options/lib.ts @@ -415,3 +415,11 @@ export async function commitEditing(): Promise { } await tick(); } + +export function fsrsParams(config: DeckConfig_Config): number[] { + if (config.fsrsParams5) { + return config.fsrsParams5; + } else { + return config.fsrsParams4; + } +}