mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Use separate field to store FSRS params
Will allow the user to keep using old params with older clients
This commit is contained in:
parent
26ae51fafd
commit
c45fa518d2
9 changed files with 68 additions and 28 deletions
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue