Use separate field to store FSRS params

Will allow the user to keep using old params with older clients
This commit is contained in:
Damien Elmes 2024-10-21 17:45:45 +10:00
parent 26ae51fafd
commit c45fa518d2
9 changed files with 68 additions and 28 deletions

View file

@ -107,9 +107,11 @@ message DeckConfig {
repeated float learn_steps = 1; repeated float learn_steps = 1;
repeated float relearn_steps = 2; 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 new_per_day = 9;
uint32 reviews_per_day = 10; uint32 reviews_per_day = 10;

View file

@ -74,7 +74,8 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
bury_new: false, bury_new: false,
bury_reviews: false, bury_reviews: false,
bury_interday_learning: false, bury_interday_learning: false,
fsrs_weights: vec![], fsrs_params_4: vec![],
fsrs_params_5: vec![],
desired_retention: 0.9, desired_retention: 0.9,
other: Vec::new(), other: Vec::new(),
historical_retention: 0.9, historical_retention: 0.9,
@ -105,6 +106,15 @@ impl DeckConfig {
self.mtime_secs = TimestampSecs::now(); self.mtime_secs = TimestampSecs::now();
self.usn = usn; self.usn = usn;
} }
/// Retrieve the FSRS 5.0 params, falling back on 4.x ones.
pub fn fsrs_params(&self) -> &Vec<f32> {
if self.inner.fsrs_params_5.len() == 19 {
&self.inner.fsrs_params_5
} else {
&self.inner.fsrs_params_4
}
}
} }
impl Collection { impl Collection {

View file

@ -69,8 +69,10 @@ pub struct DeckConfSchema11 {
#[serde(default)] #[serde(default)]
bury_interday_learning: bool, bury_interday_learning: bool,
#[serde(default, rename = "fsrsWeights")]
fsrs_params_4: Vec<f32>,
#[serde(default)] #[serde(default)]
fsrs_weights: Vec<f32>, fsrs_params_5: Vec<f32>,
#[serde(default)] #[serde(default)]
desired_retention: f32, desired_retention: f32,
#[serde(default)] #[serde(default)]
@ -306,7 +308,8 @@ impl Default for DeckConfSchema11 {
new_sort_order: 0, new_sort_order: 0,
new_gather_priority: 0, new_gather_priority: 0,
bury_interday_learning: false, bury_interday_learning: false,
fsrs_weights: vec![], fsrs_params_4: vec![],
fsrs_params_5: vec![],
desired_retention: 0.9, desired_retention: 0.9,
sm2_retention: 0.9, sm2_retention: 0.9,
weight_search: "".to_string(), weight_search: "".to_string(),
@ -386,7 +389,8 @@ impl From<DeckConfSchema11> for DeckConfig {
bury_new: c.new.bury, bury_new: c.new.bury,
bury_reviews: c.rev.bury, bury_reviews: c.rev.bury,
bury_interday_learning: c.bury_interday_learning, 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, ignore_revlogs_before_date: c.ignore_revlogs_before_date,
easy_days_percentages: c.easy_days_percentages, easy_days_percentages: c.easy_days_percentages,
desired_retention: c.desired_retention, desired_retention: c.desired_retention,
@ -498,7 +502,8 @@ impl From<DeckConfig> for DeckConfSchema11 {
new_sort_order: i.new_card_sort_order, new_sort_order: i.new_card_sort_order,
new_gather_priority: i.new_card_gather_priority, new_gather_priority: i.new_card_gather_priority,
bury_interday_learning: i.bury_interday_learning, 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, desired_retention: i.desired_retention,
sm2_retention: i.historical_retention, sm2_retention: i.historical_retention,
weight_search: i.weight_search, weight_search: i.weight_search,
@ -526,6 +531,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
"interdayLearningMix", "interdayLearningMix",
"newGatherPriority", "newGatherPriority",
"fsrsWeights", "fsrsWeights",
"fsrsParams5",
"desiredRetention", "desiredRetention",
"stopTimerOnAnswer", "stopTimerOnAnswer",
"secondsToShowQuestion", "secondsToShowQuestion",

View file

@ -50,7 +50,7 @@ impl Collection {
deck: DeckId, deck: DeckId,
) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> { ) -> Result<anki_proto::deck_config::DeckConfigsForUpdate> {
let mut defaults = DeckConfig::default(); 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 last_optimize = self.get_config_i32(I32ConfigKey::LastFsrsOptimize) as u32;
let days_since_last_fsrs_optimize = if last_optimize > 0 { let days_since_last_fsrs_optimize = if last_optimize > 0 {
self.timing_today()? self.timing_today()?
@ -88,6 +88,12 @@ impl Collection {
// grab the config and sort it // grab the config and sort it
let mut config = self.storage.all_deck_config()?; let mut config = self.storage.all_deck_config()?;
config.sort_unstable_by(|a, b| a.name.cmp(&b.name)); 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 // combine with use counts
let counts = self.get_deck_config_use_counts()?; let counts = self.get_deck_config_use_counts()?;
@ -159,8 +165,14 @@ impl Collection {
// add/update provided configs // add/update provided configs
for conf in &mut req.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 // 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)?; self.add_or_update_deck_config(conf)?;
configs_after_update.insert(conf.id, conf.clone()); configs_after_update.insert(conf.id, conf.clone());
} }
@ -201,7 +213,7 @@ impl Collection {
let previous_order = previous_config let previous_order = previous_config
.map(|c| c.inner.new_card_insert_order()) .map(|c| c.inner.new_card_insert_order())
.unwrap_or_default(); .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); 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 // 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 // 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); let current_retention = current_config.map(|c| c.inner.desired_retention);
if fsrs_toggled if fsrs_toggled
|| previous_weights != current_weights || previous_weights != current_weights
@ -252,7 +264,7 @@ impl Collection {
let weights = config.and_then(|c| { let weights = config.and_then(|c| {
if req.fsrs { if req.fsrs {
Some(UpdateMemoryStateRequest { Some(UpdateMemoryStateRequest {
weights: c.inner.fsrs_weights.clone(), weights: c.fsrs_params().clone(),
desired_retention: c.inner.desired_retention, desired_retention: c.inner.desired_retention,
max_interval: c.inner.maximum_review_interval, max_interval: c.inner.maximum_review_interval,
reschedule: req.fsrs_reschedule, reschedule: req.fsrs_reschedule,
@ -349,11 +361,11 @@ impl Collection {
ignore_revlogs_before_ms, ignore_revlogs_before_ms,
idx as u32 + 1, idx as u32 + 1,
config_len, config_len,
&config.inner.fsrs_weights, config.fsrs_params(),
) { ) {
Ok(weights) => { Ok(weights) => {
println!("{}: {:?}", config.name, weights.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(AnkiError::Interrupted) => return Err(AnkiError::Interrupted),
Err(err) => { Err(err) => {

View file

@ -431,7 +431,7 @@ impl Collection {
let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?;
let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs); let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs);
let fsrs_next_states = if fsrs_enabled { 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 { if card.memory_state.is_none() && card.ctype != CardType::New {
// Card has been moved or imported into an FSRS deck after weights were set, // 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 // and will need its initial memory state to be calculated based on review

View file

@ -154,7 +154,7 @@ impl Collection {
.or_not_found(conf_id)?; .or_not_found(conf_id)?;
let desired_retention = config.inner.desired_retention; let desired_retention = config.inner.desired_retention;
let historical_retention = config.inner.historical_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 revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?;
let item = single_card_revlog_to_item( let item = single_card_revlog_to_item(
&fsrs, &fsrs,

View file

@ -130,7 +130,7 @@ impl Collection {
.get_deck_config(conf_id)? .get_deck_config(conf_id)?
.or_not_found(conf_id)?; .or_not_found(conf_id)?;
let historical_retention = config.inner.historical_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 next_day_at = self.timing_today()?.next_day_at; let next_day_at = self.timing_today()?.next_day_at;
let ignore_before = ignore_revlogs_before_ms_from_config(&config)?; let ignore_before = ignore_revlogs_before_ms_from_config(&config)?;

View file

@ -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 SwitchRow from "$lib/components/SwitchRow.svelte";
import GlobalLabel from "./GlobalLabel.svelte"; import GlobalLabel from "./GlobalLabel.svelte";
import type { DeckOptionsState } from "./lib"; import { fsrsParams, type DeckOptionsState } from "./lib";
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
import SpinBoxRow from "./SpinBoxRow.svelte"; import SpinBoxRow from "./SpinBoxRow.svelte";
import Warning from "./Warning.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({ const simulateFsrsRequest = new SimulateFsrsReviewRequest({
weights: $config.fsrsWeights, weights: fsrsParams($config),
desiredRetention: $config.desiredRetention, desiredRetention: $config.desiredRetention,
deckSize: 0, deckSize: 0,
daysToSimulate: 365, daysToSimulate: 365,
@ -137,26 +137,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
try { try {
await runWithBackendProgress( await runWithBackendProgress(
async () => { async () => {
const params = fsrsParams($config);
const resp = await computeFsrsWeights({ const resp = await computeFsrsWeights({
search: $config.weightSearch search: $config.weightSearch
? $config.weightSearch ? $config.weightSearch
: defaultWeightSearch, : defaultWeightSearch,
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
currentWeights: $config.fsrsWeights, currentWeights: params,
}); });
if ( if (
($config.fsrsWeights.length && (params.length &&
$config.fsrsWeights.every( params.every(
(n, i) => n.toFixed(4) === resp.weights[i].toFixed(4), (n, i) => n.toFixed(4) === resp.weights[i].toFixed(4),
)) || )) ||
resp.weights.length === 0 resp.weights.length === 0
) { ) {
setTimeout(() => alert(tr.deckConfigFsrsParamsOptimal()), 100); setTimeout(() => alert(tr.deckConfigFsrsParamsOptimal()), 100);
} else {
$config.fsrsParams5 = resp.weights;
} }
if (computeWeightsProgress) { if (computeWeightsProgress) {
computeWeightsProgress.current = computeWeightsProgress.total; computeWeightsProgress.current = computeWeightsProgress.total;
} }
$config.fsrsWeights = resp.weights;
}, },
(progress) => { (progress) => {
if (progress.value.case === "computeWeights") { 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 ? $config.weightSearch
: defaultWeightSearch; : defaultWeightSearch;
const resp = await evaluateWeights({ const resp = await evaluateWeights({
weights: $config.fsrsWeights, weights: fsrsParams($config),
search, search,
ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(),
}); });
@ -230,7 +232,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
await runWithBackendProgress( await runWithBackendProgress(
async () => { async () => {
optimalRetentionRequest.maxInterval = $config.maximumReviewInterval; optimalRetentionRequest.maxInterval = $config.maximumReviewInterval;
optimalRetentionRequest.weights = $config.fsrsWeights; optimalRetentionRequest.weights = fsrsParams($config);
optimalRetentionRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`; optimalRetentionRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`;
const resp = await computeOptimalRetention(optimalRetentionRequest); const resp = await computeOptimalRetention(optimalRetentionRequest);
optimalRetention = resp.optimalRetention; optimalRetention = resp.optimalRetention;
@ -311,7 +313,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
try { try {
await runWithBackendProgress( await runWithBackendProgress(
async () => { async () => {
simulateFsrsRequest.weights = $config.fsrsWeights; simulateFsrsRequest.weights = fsrsParams($config);
simulateFsrsRequest.desiredRetention = $config.desiredRetention; simulateFsrsRequest.desiredRetention = $config.desiredRetention;
simulateFsrsRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`; simulateFsrsRequest.search = `preset:"${state.getCurrentName()}" -is:suspended`;
simulateProgressString = "processing..."; simulateProgressString = "processing...";
@ -360,9 +362,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<div class="ms-1 me-1"> <div class="ms-1 me-1">
<WeightsInputRow <WeightsInputRow
bind:value={$config.fsrsWeights} bind:value={$config.fsrsParams5}
defaultValue={[]} defaultValue={[]}
defaults={defaults.fsrsWeights} defaults={defaults.fsrsParams5}
> >
<SettingTitle on:click={() => openHelpModal("modelWeights")}> <SettingTitle on:click={() => openHelpModal("modelWeights")}>
{tr.deckConfigWeights()} {tr.deckConfigWeights()}

View file

@ -415,3 +415,11 @@ export async function commitEditing(): Promise<void> {
} }
await tick(); await tick();
} }
export function fsrsParams(config: DeckConfig_Config): number[] {
if (config.fsrsParams5) {
return config.fsrsParams5;
} else {
return config.fsrsParams4;
}
}