From 5679e8453e61eadc3485c8123cd62884d599646e Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 3 Apr 2025 13:37:03 +0800 Subject: [PATCH 01/12] Feat/FSRS-6 --- Cargo.lock | 82 +++++++++++++---------- Cargo.toml | 6 +- proto/anki/deck_config.proto | 3 +- proto/anki/scheduler.proto | 21 +++--- rslib/src/deckconfig/mod.rs | 7 +- rslib/src/deckconfig/schema11.rs | 5 ++ rslib/src/deckconfig/update.rs | 15 +++-- rslib/src/scheduler/fsrs/retention.rs | 11 ++- rslib/src/scheduler/fsrs/simulator.rs | 11 ++- rslib/src/scheduler/service/mod.rs | 26 +++++-- ts/routes/card-info/Revlog.svelte | 11 ++- ts/routes/deck-options/FsrsOptions.svelte | 6 +- ts/routes/deck-options/lib.ts | 4 +- 13 files changed, 126 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32f15ab2bea..481dd6106ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,7 +142,7 @@ dependencies = [ "serde_tuple", "sha1", "snafu", - "strum", + "strum 0.26.3", "syn 2.0.96", "tempfile", "tokio", @@ -218,7 +218,7 @@ dependencies = [ "prost-types", "serde", "snafu", - "strum", + "strum 0.26.3", ] [[package]] @@ -774,8 +774,8 @@ dependencies = [ "sanitize-filename 0.6.0", "serde", "serde_json", - "strum", - "strum_macros", + "strum 0.26.3", + "strum_macros 0.26.4", "tempfile", "thiserror 2.0.11", ] @@ -842,7 +842,7 @@ dependencies = [ "derive-new 0.7.0", "libm", "matrixmultiply", - "ndarray 0.16.1", + "ndarray", "num-traits", "portable-atomic-util", "rand", @@ -2099,19 +2099,18 @@ dependencies = [ [[package]] name = "fsrs" version = "3.0.0" -source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=08d90d1363b0c4722422bf0ef71ed8fd7d053f8a#08d90d1363b0c4722422bf0ef71ed8fd7d053f8a" dependencies = [ "burn", - "itertools 0.12.1", + "itertools 0.14.0", "log", - "ndarray 0.15.6", + "ndarray", "ndarray-rand", "priority-queue", "rand", "rayon", "serde", "snafu", - "strum", + "strum 0.27.1", ] [[package]] @@ -3254,18 +3253,18 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -3441,7 +3440,7 @@ dependencies = [ "linkcheck", "regex", "reqwest 0.12.8", - "strum", + "strum 0.26.3", "tokio", ] @@ -3831,19 +3830,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ndarray" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "rawpointer", -] - [[package]] name = "ndarray" version = "0.16.1" @@ -3862,11 +3848,11 @@ dependencies = [ [[package]] name = "ndarray-rand" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65608f937acc725f5b164dcf40f4f0bc5d67dc268ab8a649d3002606718c4588" +checksum = "f093b3db6fd194718dcdeea6bd8c829417deae904e3fcc7732dabcd4416d25d8" dependencies = [ - "ndarray 0.15.6", + "ndarray", "rand", "rand_distr", ] @@ -4608,9 +4594,9 @@ dependencies = [ [[package]] name = "priority-queue" -version = "2.1.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" +checksum = "ef08705fa1589a1a59aa924ad77d14722cb0cd97b67dd5004ed5f4a4873fce8d" dependencies = [ "autocfg", "equivalent", @@ -5561,9 +5547,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -5590,9 +5576,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -5869,7 +5855,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros 0.27.1", ] [[package]] @@ -5885,6 +5880,19 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.96", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index c16294236c7..b774b4d2b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,9 +36,9 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] # version = "=2.0.3" -git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a" -# path = "../open-spaced-repetition/fsrs-rs" +# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" +# rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a" +path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] # local diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index efd8a80d080..5a2b35083d0 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -110,9 +110,10 @@ message DeckConfig { repeated float fsrs_params_4 = 3; repeated float fsrs_params_5 = 5; + repeated float fsrs_params_6 = 6; // consider saving remaining ones for fsrs param changes - reserved 6 to 8; + reserved 7 to 8; uint32 new_per_day = 9; uint32 reviews_per_day = 10; diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 124f63d56b6..5eefe003055 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -432,17 +432,16 @@ message GetOptimalRetentionParametersResponse { uint32 learn_span = 2; float max_cost_perday = 3; float max_ivl = 4; - repeated float learn_costs = 5; - repeated float review_costs = 6; - repeated float first_rating_prob = 7; - repeated float review_rating_prob = 8; - repeated float first_rating_offsets = 9; - repeated float first_session_lens = 10; - float forget_rating_offset = 11; - float forget_session_len = 12; - float loss_aversion = 13; - uint32 learn_limit = 14; - uint32 review_limit = 15; + repeated float first_rating_prob = 5; + repeated float review_rating_prob = 6; + float loss_aversion = 7; + uint32 learn_limit = 8; + uint32 review_limit = 9; + repeated float learning_step_transitions = 10; + repeated float relearning_step_transitions = 11; + repeated float state_rating_costs = 12; + uint32 learning_step_count = 13; + uint32 relearning_step_count = 14; } message EvaluateParamsRequest { diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index 21a75f521ec..beefc9adfff 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -76,6 +76,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner { bury_interday_learning: false, fsrs_params_4: vec![], fsrs_params_5: vec![], + fsrs_params_6: vec![], desired_retention: 0.9, other: Vec::new(), historical_retention: 0.9, @@ -107,9 +108,11 @@ impl DeckConfig { self.usn = usn; } - /// Retrieve the FSRS 5.0 params, falling back on 4.x ones. + /// Retrieve the FSRS 6.0 params, falling back on 5.0 or 4.0 ones. pub fn fsrs_params(&self) -> &Vec { - if self.inner.fsrs_params_5.len() == 19 { + if self.inner.fsrs_params_6.len() == 20 { + &self.inner.fsrs_params_6 + } else if self.inner.fsrs_params_5.len() == 19 { &self.inner.fsrs_params_5 } else { &self.inner.fsrs_params_4 diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index 73800f38ea6..d06525779be 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -74,6 +74,8 @@ pub struct DeckConfSchema11 { #[serde(default)] fsrs_params_5: Vec, #[serde(default)] + fsrs_params_6: Vec, + #[serde(default)] desired_retention: f32, #[serde(default)] ignore_revlogs_before_date: String, @@ -310,6 +312,7 @@ impl Default for DeckConfSchema11 { bury_interday_learning: false, fsrs_params_4: vec![], fsrs_params_5: vec![], + fsrs_params_6: vec![], desired_retention: 0.9, sm2_retention: 0.9, param_search: "".to_string(), @@ -391,6 +394,7 @@ impl From for DeckConfig { bury_interday_learning: c.bury_interday_learning, fsrs_params_4: c.fsrs_params_4, fsrs_params_5: c.fsrs_params_5, + fsrs_params_6: c.fsrs_params_6, ignore_revlogs_before_date: c.ignore_revlogs_before_date, easy_days_percentages: c.easy_days_percentages, desired_retention: c.desired_retention, @@ -504,6 +508,7 @@ impl From for DeckConfSchema11 { bury_interday_learning: i.bury_interday_learning, fsrs_params_4: i.fsrs_params_4, fsrs_params_5: i.fsrs_params_5, + fsrs_params_6: i.fsrs_params_6, desired_retention: i.desired_retention, sm2_retention: i.historical_retention, param_search: i.param_search, diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index 95678699552..1c9c047218b 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_params_5 = DEFAULT_PARAMETERS.into(); + defaults.inner.fsrs_params_6 = 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()? @@ -90,8 +90,12 @@ impl Collection { 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(); + if c.inner.fsrs_params_6.is_empty() { + c.inner.fsrs_params_6 = if c.inner.fsrs_params_5.is_empty() { + c.inner.fsrs_params_4.clone() + } else { + c.inner.fsrs_params_5.clone() + }; } }); @@ -168,7 +172,8 @@ impl Collection { // 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() { + if conf.inner.fsrs_params_6.is_empty() { + conf.inner.fsrs_params_5.clear(); conf.inner.fsrs_params_4.clear(); } // check the provided parameters are valid before we save them @@ -370,7 +375,7 @@ impl Collection { ) { Ok(params) => { println!("{}: {:?}", config.name, params.params); - config.inner.fsrs_params_5 = params.params; + config.inner.fsrs_params_6 = params.params; } Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted), Err(err) => { diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index 23df5b33a1e..e4190416047 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -66,14 +66,8 @@ impl Collection { learn_span: req.days_to_simulate as usize, max_cost_perday: f32::MAX, max_ivl: req.max_interval as f32, - learn_costs: p.learn_costs, - review_costs: p.review_costs, first_rating_prob: p.first_rating_prob, review_rating_prob: p.review_rating_prob, - first_rating_offsets: p.first_rating_offsets, - first_session_lens: p.first_session_lens, - forget_rating_offset: p.forget_rating_offset, - forget_session_len: p.forget_session_len, loss_aversion: req.loss_aversion as f32, learn_limit, review_limit: usize::MAX, @@ -81,6 +75,11 @@ impl Collection { suspend_after_lapses: None, post_scheduling_fn, review_priority_fn: None, + learning_step_transitions: p.learning_step_transitions, + relearning_step_transitions: p.relearning_step_transitions, + state_rating_costs: p.state_rating_costs, + learning_step_count: p.learning_step_count, + relearning_step_count: p.relearning_step_count, }, &req.params, |ip| { diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 34bec1317f9..dad1ec8ada2 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -188,14 +188,8 @@ impl Collection { learn_span: req.days_to_simulate as usize, max_cost_perday: f32::MAX, max_ivl: req.max_interval as f32, - learn_costs: p.learn_costs, - review_costs: p.review_costs, first_rating_prob: p.first_rating_prob, review_rating_prob: p.review_rating_prob, - first_rating_offsets: p.first_rating_offsets, - first_session_lens: p.first_session_lens, - forget_rating_offset: p.forget_rating_offset, - forget_session_len: p.forget_session_len, loss_aversion: 1.0, learn_limit: req.new_limit as usize, review_limit: req.review_limit as usize, @@ -203,6 +197,11 @@ impl Collection { suspend_after_lapses: req.suspend_after_lapse_count, post_scheduling_fn, review_priority_fn, + learning_step_transitions: p.learning_step_transitions, + relearning_step_transitions: p.relearning_step_transitions, + state_rating_costs: p.state_rating_costs, + learning_step_count: p.learning_step_count, + relearning_step_count: p.relearning_step_count, }; let result = simulate( &config, diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index d398ae65bdf..526caad3055 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -321,17 +321,31 @@ impl crate::services::SchedulerService for Collection { learn_span: simulator_config.learn_span as u32, max_cost_perday: simulator_config.max_cost_perday, max_ivl: simulator_config.max_ivl, - learn_costs: simulator_config.learn_costs.to_vec(), - review_costs: simulator_config.review_costs.to_vec(), first_rating_prob: simulator_config.first_rating_prob.to_vec(), review_rating_prob: simulator_config.review_rating_prob.to_vec(), - first_rating_offsets: simulator_config.first_rating_offsets.to_vec(), - first_session_lens: simulator_config.first_session_lens.to_vec(), - forget_rating_offset: simulator_config.forget_rating_offset, - forget_session_len: simulator_config.forget_session_len, loss_aversion: simulator_config.loss_aversion, learn_limit: simulator_config.learn_limit as u32, review_limit: simulator_config.review_limit as u32, + learning_step_transitions: simulator_config + .learning_step_transitions + .iter() + .flatten() + .cloned() + .collect(), + relearning_step_transitions: simulator_config + .relearning_step_transitions + .iter() + .flatten() + .cloned() + .collect(), + state_rating_costs: simulator_config + .state_rating_costs + .iter() + .flatten() + .cloned() + .collect(), + learning_step_count: simulator_config.learning_step_count as u32, + relearning_step_count: simulator_config.relearning_step_count as u32, }) } diff --git a/ts/routes/card-info/Revlog.svelte b/ts/routes/card-info/Revlog.svelte index dc410b4ab20..b02caa3de22 100644 --- a/ts/routes/card-info/Revlog.svelte +++ b/ts/routes/card-info/Revlog.svelte @@ -174,7 +174,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/each} - {#if fsrsEnabled}{/if} + {/if} diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 82c822a6652..224c8de051d 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -167,7 +167,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html : tr.deckConfigFsrsParamsNoReviews(); setTimeout(() => alert(msg), 200); } else { - $config.fsrsParams5 = resp.params; + $config.fsrsParams6 = resp.params; } if (computeParamsProgress) { computeParamsProgress.current = computeParamsProgress.total; @@ -322,9 +322,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
openHelpModal("modelParams")}> {tr.deckConfigWeights()} diff --git a/ts/routes/deck-options/lib.ts b/ts/routes/deck-options/lib.ts index da471e027f2..c9eabb0f7e9 100644 --- a/ts/routes/deck-options/lib.ts +++ b/ts/routes/deck-options/lib.ts @@ -461,7 +461,9 @@ export async function commitEditing(): Promise { } export function fsrsParams(config: DeckConfig_Config): number[] { - if (config.fsrsParams5) { + if (config.fsrsParams6) { + return config.fsrsParams6; + } else if (config.fsrsParams5) { return config.fsrsParams5; } else { return config.fsrsParams4; From d822b4a2fef2db789d14a2fd9a7631de517ed370 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 3 Apr 2025 14:05:28 +0800 Subject: [PATCH 02/12] update comment --- rslib/src/deckconfig/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index beefc9adfff..5ccac05217f 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -108,7 +108,7 @@ impl DeckConfig { self.usn = usn; } - /// Retrieve the FSRS 6.0 params, falling back on 5.0 or 4.0 ones. + /// Retrieve the FSRS 6.0 params, falling back on 5.0 or 4.x ones. pub fn fsrs_params(&self) -> &Vec { if self.inner.fsrs_params_6.len() == 20 { &self.inner.fsrs_params_6 From 50c0b5f0b659b7ba614d68668093af6b5a4c39eb Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 17 Apr 2025 18:04:37 +0800 Subject: [PATCH 03/12] add decay to Card --- Cargo.lock | 71 ++++++++++++---------- Cargo.toml | 2 +- proto/anki/cards.proto | 1 + proto/anki/stats.proto | 1 + rslib/src/browser_table.rs | 11 ++-- rslib/src/card/mod.rs | 2 + rslib/src/card/service.rs | 2 + rslib/src/deckconfig/mod.rs | 2 +- rslib/src/deckconfig/schema11.rs | 2 +- rslib/src/scheduler/fsrs/memory_state.rs | 10 +++ rslib/src/scheduler/fsrs/retention.rs | 1 - rslib/src/scheduler/fsrs/simulator.rs | 11 ++-- rslib/src/scheduler/service/mod.rs | 2 +- rslib/src/stats/card.rs | 8 ++- rslib/src/stats/graphs/retrievability.rs | 6 +- rslib/src/storage/card/data.rs | 13 +++- rslib/src/storage/card/mod.rs | 1 + rslib/src/storage/sqlite.rs | 6 +- rslib/src/sync/collection/chunks.rs | 1 + ts/routes/card-info/CardInfo.svelte | 8 ++- ts/routes/card-info/ForgettingCurve.svelte | 2 + ts/routes/card-info/forgetting-curve.ts | 21 ++++--- ts/routes/deck-options/FsrsOptions.svelte | 9 ++- 23 files changed, 126 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 481dd6106ea..2dcf4d83a60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,11 +574,12 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bincode" -version = "2.0.0-rc.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" dependencies = [ "serde", + "unty", ] [[package]] @@ -664,9 +665,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "burn" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55af4c56b540bcf00cf1c7e13b1c60644734906495048afbd4a79aabd0a6efbe" +checksum = "22149f3b5ab6628e9e9c0b29156b906d32d36bbf76f2c34ad5ce1801f5b4486e" dependencies = [ "burn-core", "burn-train", @@ -674,9 +675,9 @@ dependencies = [ [[package]] name = "burn-autodiff" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa53181463ef16220438e240f10e1e8cb2fcf1824dbc33b8f259a454ff5f46f" +checksum = "f2167ab07f9be5f2a027accba92d8dde02ea905f35844f8529bb2533b4fc8646" dependencies = [ "burn-common", "burn-tensor", @@ -687,9 +688,9 @@ dependencies = [ [[package]] name = "burn-candle" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b49a6da72c10ac552b3c023d74dade9714c10aac0fc5f33cfc4ca389463b99e" +checksum = "aeef1204c4d33dd71a9628a311178eb149131c65234eb64e8201e27cf1ee1ba0" dependencies = [ "burn-tensor", "candle-core", @@ -699,9 +700,9 @@ dependencies = [ [[package]] name = "burn-common" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1471949b06002c984df9d753a084a79149841dd7935911d9e432b8478f9fd5" +checksum = "fb516d1faa50628828b3c2b79db3e483f20d62966f7dae68c6f21743f5f7e8ef" dependencies = [ "cubecl-common", "getrandom 0.2.15", @@ -712,9 +713,9 @@ dependencies = [ [[package]] name = "burn-core" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8ebbf7d5c8bdc269260bd8e7ce08e488e6625da19b3d80ca34a729d78a77ab" +checksum = "594c44ac9f2996c2c0b92f5a44a1287d41fca3954182601a4a29b628a5973357" dependencies = [ "ahash", "bincode", @@ -747,9 +748,9 @@ dependencies = [ [[package]] name = "burn-cuda" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90534d6c7f909a8cad49470921dc3eb2b118f7a3c8bde313defba3f4cae3ac3" +checksum = "08fe1e5f285214d16cfd298453b807675c9a4ed742a35c8807be42af69e8ee97" dependencies = [ "burn-jit", "burn-tensor", @@ -762,9 +763,9 @@ dependencies = [ [[package]] name = "burn-dataset" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b851cb5165da57871bed2c48a29673dde0ddbd198a39b2a37411b6adf6df6ad" +checksum = "92a5cde6c09c751fb6aafca10d8e18faa42fe18eef44b4768851575c20db6904" dependencies = [ "csv", "derive-new 0.7.0", @@ -782,9 +783,9 @@ dependencies = [ [[package]] name = "burn-derive" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f784ffe0df57848ba232e5f40a1c1f5df3571df59bec99ba32bc7610fd9e811" +checksum = "2bb8f828a681946b07a87750ed0593d885e7b101653bd6a3bb1942976156bb48" dependencies = [ "derive-new 0.7.0", "proc-macro2", @@ -794,9 +795,9 @@ dependencies = [ [[package]] name = "burn-hip" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd9fbfee77b3d2b67bf434b883ec6ed73f4f9bf1fd8d59f9dde217c7a4b5285d" +checksum = "84191ed69af8c48a133c05e0ee4dfe73d7a3b8f96e3ceec899b4f85b19072232" dependencies = [ "burn-jit", "burn-tensor", @@ -809,9 +810,9 @@ dependencies = [ [[package]] name = "burn-jit" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6b06689c4e8d6cfdcaf0b0e168e58a931c3935414e48f4e3e3e85e8d7a77a0" +checksum = "6b723ddb46032953c4fb908feca57b470b0c9839f808fcd52de04a8510b88a23" dependencies = [ "burn-common", "burn-tensor", @@ -831,9 +832,9 @@ dependencies = [ [[package]] name = "burn-ndarray" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419fa3eda8cf9fddce0d156946b3d46642c10a41569b23e7855f775f862d310a" +checksum = "1b8ce3bd0f1e792b53610d291eb463d9790449688c4455a496c46266e46a179f" dependencies = [ "atomic_float", "burn-autodiff", @@ -851,9 +852,9 @@ dependencies = [ [[package]] name = "burn-router" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39bdb6d5c749221741a362da9b3ea3157304f831ab4b4a6902725a1efaea159" +checksum = "b6a1a6cb08eccb65b112bc5853ac35c88faa8b04b03270bcfb1ac8a253a066bb" dependencies = [ "burn-common", "burn-tensor", @@ -864,9 +865,9 @@ dependencies = [ [[package]] name = "burn-tensor" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24db20273a636d5340e5a29af142722e0a657491e6b3cfcceb1e62eb862b3b37" +checksum = "ab959e7da2e7514b959d841c93e8e026233aa77284f5d976099a8f5251e3ba99" dependencies = [ "burn-common", "bytemuck", @@ -885,9 +886,9 @@ dependencies = [ [[package]] name = "burn-train" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714298cbc0c41f48d53cb1e6aeb6203b49b6110620517f69fbcc37a9b41cb6c8" +checksum = "c004e8c761ad50c568739581a2dab38aa2f4db723183fb189f130e6d2b4e0d1c" dependencies = [ "async-channel", "burn-core", @@ -906,9 +907,9 @@ dependencies = [ [[package]] name = "burn-wgpu" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef5b6c56da563a708b2da16f0559a061e7b93f3acae63903734ee978c9b9f93" +checksum = "5f80a3413527087e73042c807d41d6fd3a1e8a28eb519e3ad5e91f6354cbfb9d" dependencies = [ "burn-jit", "burn-tensor", @@ -6710,6 +6711,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" diff --git a/Cargo.toml b/Cargo.toml index b774b4d2b32..34a0ae3011f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] # version = "=2.0.3" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -# rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a" +# rev = "fc787fe176975d360e8a7ae0c6f5fc9ffadccd16" path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] diff --git a/proto/anki/cards.proto b/proto/anki/cards.proto index eef1b8264c0..c120440e843 100644 --- a/proto/anki/cards.proto +++ b/proto/anki/cards.proto @@ -50,6 +50,7 @@ message Card { optional uint32 original_position = 18; optional FsrsMemoryState memory_state = 20; optional float desired_retention = 21; + optional float decay = 22; string custom_data = 19; } diff --git a/proto/anki/stats.proto b/proto/anki/stats.proto index 42d02029cea..a5639f8415b 100644 --- a/proto/anki/stats.proto +++ b/proto/anki/stats.proto @@ -65,6 +65,7 @@ message CardStatsResponse { string preset = 21; optional string original_deck = 22; optional float desired_retention = 23; + repeated float fsrs_params = 24; } message GraphsRequest { diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 4680caa0dc7..176b506a5ae 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -541,10 +541,13 @@ impl RowContext { .memory_state .as_ref() .zip(self.cards[0].days_since_last_review(&self.timing)) - .map(|(state, days_elapsed)| { - let r = FSRS::new(None) - .unwrap() - .current_retrievability((*state).into(), days_elapsed); + .zip(Some(self.cards[0].decay.unwrap_or(0.5))) + .map(|((state, days_elapsed), decay)| { + let r = FSRS::new(None).unwrap().current_retrievability( + (*state).into(), + days_elapsed, + decay, + ); format!("{:.0}%", r * 100.) }) .unwrap_or_default() diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index 98b7ef90c64..8d7821e2ce4 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -95,6 +95,7 @@ pub struct Card { pub(crate) original_position: Option, pub(crate) memory_state: Option, pub(crate) desired_retention: Option, + pub(crate) decay: Option, /// JSON object or empty; exposed through the reviewer for persisting custom /// state pub(crate) custom_data: String, @@ -145,6 +146,7 @@ impl Default for Card { original_position: None, memory_state: None, desired_retention: None, + decay: None, custom_data: String::new(), } } diff --git a/rslib/src/card/service.rs b/rslib/src/card/service.rs index 63824b7897e..8f1421f25c4 100644 --- a/rslib/src/card/service.rs +++ b/rslib/src/card/service.rs @@ -106,6 +106,7 @@ impl TryFrom for Card { original_position: c.original_position, memory_state: c.memory_state.map(Into::into), desired_retention: c.desired_retention, + decay: c.decay, custom_data: c.custom_data, }) } @@ -134,6 +135,7 @@ impl From for anki_proto::cards::Card { original_position: c.original_position, memory_state: c.memory_state.map(Into::into), desired_retention: c.desired_retention, + decay: c.decay, custom_data: c.custom_data, } } diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index 5ccac05217f..c522ea18a57 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -110,7 +110,7 @@ impl DeckConfig { /// Retrieve the FSRS 6.0 params, falling back on 5.0 or 4.x ones. pub fn fsrs_params(&self) -> &Vec { - if self.inner.fsrs_params_6.len() == 20 { + if self.inner.fsrs_params_6.len() == 21 { &self.inner.fsrs_params_6 } else if self.inner.fsrs_params_5.len() == 19 { &self.inner.fsrs_params_5 diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index d06525779be..2d825d4ef91 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -536,7 +536,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! { "interdayLearningMix", "newGatherPriority", "fsrsWeights", - "fsrsParams5", + "fsrsParams6", "desiredRetention", "stopTimerOnAnswer", "secondsToShowQuestion", diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 90920f4bbcf..3d59825d02d 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -76,6 +76,15 @@ impl Collection { .then(|| Rescheduler::new(self)) .transpose()?; let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?; + let decay = req.as_ref().map(|w| { + if w.params.is_empty() { + 0.2 // default decay for FSRS-6 + } else if w.params.len() < 21 { + 0.5 // default decay for FSRS-4.5 and FSRS-5 + } else { + w.params[20] + } + }); let historical_retention = req.as_ref().map(|w| w.historical_retention); let items = fsrs_items_for_memory_states( &fsrs, @@ -94,6 +103,7 @@ impl Collection { if let (Some(req), Some(item)) = (&req, item) { card.set_memory_state(&fsrs, Some(item), historical_retention.unwrap())?; card.desired_retention = desired_retention; + card.decay = decay; // if rescheduling if let Some(reviews) = &last_revlog_info { // and we have a last review time for the card diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index e4190416047..27ba5a99724 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -68,7 +68,6 @@ impl Collection { max_ivl: req.max_interval as f32, first_rating_prob: p.first_rating_prob, review_rating_prob: p.review_rating_prob, - loss_aversion: req.loss_aversion as f32, learn_limit, review_limit: usize::MAX, new_cards_ignore_review_limit: true, diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index dad1ec8ada2..25492694f55 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -75,6 +75,7 @@ pub(crate) fn apply_load_balance_and_easy_days( fn create_review_priority_fn( review_order: ReviewCardOrder, deck_size: usize, + params: Vec, ) -> Option { // Helper macro to wrap closure in ReviewPriorityFn macro_rules! wrap { @@ -91,11 +92,12 @@ fn create_review_priority_fn( // Interval-based ordering IntervalsAscending => wrap!(|c| c.interval as i32), IntervalsDescending => wrap!(|c| -(c.interval as i32)), - // Retrievability-based ordering - RetrievabilityAscending => wrap!(|c| (c.retrievability() * 1000.0) as i32), + RetrievabilityAscending => { + wrap!(move |c| (c.retrievability(¶ms) * 1000.0) as i32) + } RetrievabilityDescending => { - wrap!(|c| -(c.retrievability() * 1000.0) as i32) + wrap!(move |c| -(c.retrievability(¶ms) * 1000.0) as i32) } // Due date ordering @@ -181,7 +183,7 @@ impl Collection { .review_order .try_into() .ok() - .and_then(|order| create_review_priority_fn(order, deck_size)); + .and_then(|order| create_review_priority_fn(order, deck_size, req.params.clone())); let config = SimulatorConfig { deck_size, @@ -190,7 +192,6 @@ impl Collection { max_ivl: req.max_interval as f32, first_rating_prob: p.first_rating_prob, review_rating_prob: p.review_rating_prob, - loss_aversion: 1.0, learn_limit: req.new_limit as usize, review_limit: req.review_limit as usize, new_cards_ignore_review_limit: req.new_cards_ignore_review_limit, diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 526caad3055..1d5d1ccb4e6 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -323,7 +323,7 @@ impl crate::services::SchedulerService for Collection { max_ivl: simulator_config.max_ivl, first_rating_prob: simulator_config.first_rating_prob.to_vec(), review_rating_prob: simulator_config.review_rating_prob.to_vec(), - loss_aversion: simulator_config.loss_aversion, + loss_aversion: 1.0, learn_limit: simulator_config.learn_limit as u32, review_limit: simulator_config.review_limit as u32, learning_step_transitions: simulator_config diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 6971311bf9d..74e22295207 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -37,10 +37,11 @@ impl Collection { let fsrs_retrievability = card .memory_state .zip(Some(days_elapsed)) - .map(|(state, days)| { + .zip(card.decay) + .map(|((state, days), decay)| { FSRS::new(None) .unwrap() - .current_retrievability(state.into(), days) + .current_retrievability(state.into(), days, decay) }); let original_deck = if card.original_deck_id == DeckId(0) { @@ -75,13 +76,14 @@ impl Collection { memory_state: card.memory_state.map(Into::into), fsrs_retrievability, custom_data: card.custom_data, - preset: preset.name, + preset: preset.name.clone(), original_deck: if original_deck != deck { Some(original_deck.human_name()) } else { None }, desired_retention: card.desired_retention, + fsrs_params: preset.fsrs_params().to_vec(), }) } diff --git a/rslib/src/stats/graphs/retrievability.rs b/rslib/src/stats/graphs/retrievability.rs index cd1b6105c6a..ad7f79f61d8 100644 --- a/rslib/src/stats/graphs/retrievability.rs +++ b/rslib/src/stats/graphs/retrievability.rs @@ -30,7 +30,11 @@ impl GraphsContext { entry.1 += 1; if let Some(state) = card.memory_state { let elapsed_days = card.days_since_last_review(&timing).unwrap_or_default(); - let r = fsrs.current_retrievability(state.into(), elapsed_days); + let r = fsrs.current_retrievability( + state.into(), + elapsed_days, + card.decay.unwrap_or(0.5), + ); *retrievability .retrievability diff --git a/rslib/src/storage/card/data.rs b/rslib/src/storage/card/data.rs index 8ef94146f49..3d2b4c886fe 100644 --- a/rslib/src/storage/card/data.rs +++ b/rslib/src/storage/card/data.rs @@ -42,6 +42,12 @@ pub(crate) struct CardData { deserialize_with = "default_on_invalid" )] pub(crate) fsrs_desired_retention: Option, + #[serde( + rename = "decay", + skip_serializing_if = "Option::is_none", + deserialize_with = "default_on_invalid" + )] + pub(crate) decay: Option, /// A string representation of a JSON object storing optional data /// associated with the card, so v3 custom scheduling code can persist @@ -57,6 +63,7 @@ impl CardData { fsrs_stability: card.memory_state.as_ref().map(|m| m.stability), fsrs_difficulty: card.memory_state.as_ref().map(|m| m.difficulty), fsrs_desired_retention: card.desired_retention, + decay: card.decay, custom_data: card.custom_data.clone(), } } @@ -87,6 +94,9 @@ impl CardData { if let Some(v) = &mut self.fsrs_desired_retention { round_to_places(v, 2) } + if let Some(v) = &mut self.decay { + round_to_places(v, 3) + } serde_json::to_string(&self).map_err(Into::into) } } @@ -159,11 +169,12 @@ mod test { fsrs_stability: Some(123.45678), fsrs_difficulty: Some(1.234567), fsrs_desired_retention: Some(0.987654), + decay: Some(0.123456), custom_data: "".to_string(), }; assert_eq!( data.convert_to_json().unwrap(), - r#"{"s":123.457,"d":1.235,"dr":0.99}"# + r#"{"s":123.457,"d":1.235,"dr":0.99,"decay":0.123}"# ); } } diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index a699d5ef289..d91fe65c35f 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -85,6 +85,7 @@ fn row_to_card(row: &Row) -> result::Result { original_position: data.original_position, memory_state: data.memory_state(), desired_retention: data.fsrs_desired_retention, + decay: data.decay, custom_data: data.custom_data, }) } diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index c202bb0efd0..08b7e10190d 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -325,10 +325,11 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> { let review_day = due.saturating_sub(ivl); days_elapsed.saturating_sub(review_day) as u32 }; + let decay = card_data.decay.unwrap_or(0.5); Ok(card_data.memory_state().map(|state| { FSRS::new(None) .unwrap() - .current_retrievability(state.into(), days_elapsed) + .current_retrievability(state.into(), days_elapsed, decay) })) }, ) @@ -374,10 +375,11 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result { // avoid div by zero desired_retrievability = desired_retrievability.max(0.0001); + let decay = card_data.decay.unwrap_or(0.5); let current_retrievability = FSRS::new(None) .unwrap() - .current_retrievability(state.into(), days_elapsed) + .current_retrievability(state.into(), days_elapsed, decay) .max(0.0001); return Ok(Some( diff --git a/rslib/src/sync/collection/chunks.rs b/rslib/src/sync/collection/chunks.rs index c840decf3e5..9d74ddb6c63 100644 --- a/rslib/src/sync/collection/chunks.rs +++ b/rslib/src/sync/collection/chunks.rs @@ -332,6 +332,7 @@ impl From for Card { original_position: data.original_position, memory_state: data.memory_state(), desired_retention: data.fsrs_desired_retention, + decay: data.decay, custom_data: data.custom_data, } } diff --git a/ts/routes/card-info/CardInfo.svelte b/ts/routes/card-info/CardInfo.svelte index b332f131ee5..cfb62da32ac 100644 --- a/ts/routes/card-info/CardInfo.svelte +++ b/ts/routes/card-info/CardInfo.svelte @@ -18,6 +18,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: fsrsEnabled = stats?.memoryState != null; $: desiredRetention = stats?.desiredRetention ?? 0.9; + $: decay = + (stats?.fsrsParams?.length ?? 0) === 0 + ? 0.2 + : (stats?.fsrsParams?.length ?? 0) < 21 + ? 0.5 + : (stats?.fsrsParams?.[20] ?? 0.2); @@ -33,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} {#if fsrsEnabled} - + {/if} {:else} diff --git a/ts/routes/card-info/ForgettingCurve.svelte b/ts/routes/card-info/ForgettingCurve.svelte index bb3a483f71a..19af5e703b0 100644 --- a/ts/routes/card-info/ForgettingCurve.svelte +++ b/ts/routes/card-info/ForgettingCurve.svelte @@ -21,6 +21,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let revlog: RevlogEntry[]; export let desiredRetention: number; + export let decay: number; let svg: HTMLElement | SVGElement | null = null; const bounds = defaultGraphBounds(); const title = tr.cardStatsFsrsForgettingCurveTitle(); @@ -47,6 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html svg as SVGElement, bounds, desiredRetention, + decay, ); diff --git a/ts/routes/card-info/forgetting-curve.ts b/ts/routes/card-info/forgetting-curve.ts index 9f0301fcebf..817feccb2b7 100644 --- a/ts/routes/card-info/forgetting-curve.ts +++ b/ts/routes/card-info/forgetting-curve.ts @@ -11,12 +11,11 @@ import { axisBottom, axisLeft, line, max, min, pointer, scaleLinear, scaleTime, import { type GraphBounds, setDataAvailable } from "../graphs/graph-helpers"; import { hideTooltip, showTooltip } from "../graphs/tooltip-utils.svelte"; -const FACTOR = 19 / 81; -const DECAY = -0.5; const MIN_POINTS = 1000; -function forgettingCurve(stability: number, daysElapsed: number): number { - return Math.pow((daysElapsed / stability) * FACTOR + 1.0, DECAY); +function forgettingCurve(stability: number, daysElapsed: number, decay: number): number { + const factor = Math.pow(0.9, 1 / -decay) - 1; + return Math.pow((daysElapsed / stability) * factor + 1.0, -decay); } interface DataPoint { @@ -68,7 +67,7 @@ export function filterRevlog(revlog: RevlogEntry[]): RevlogEntry[] { return result.filter((entry) => filterRevlogEntryByReviewKind(entry)); } -export function prepareData(revlog: RevlogEntry[], maxDays: number) { +export function prepareData(revlog: RevlogEntry[], maxDays: number, decay: number) { const data: DataPoint[] = []; let lastReviewTime = 0; let lastStability = 0; @@ -97,7 +96,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) { let elapsedDays = 0; while (elapsedDays < totalDaysElapsed - step) { elapsedDays += step; - const retrievability = forgettingCurve(lastStability, elapsedDays); + const retrievability = forgettingCurve(lastStability, elapsedDays, decay); data.push({ date: new Date((lastReviewTime + elapsedDays * 86400) * 1000), daysSinceFirstLearn: data[data.length - 1].daysSinceFirstLearn + step, @@ -128,7 +127,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) { let elapsedDays = 0; while (elapsedDays < totalDaysSinceLastReview - step) { elapsedDays += step; - const retrievability = forgettingCurve(lastStability, elapsedDays); + const retrievability = forgettingCurve(lastStability, elapsedDays, decay); data.push({ date: new Date((lastReviewTime + elapsedDays * 86400) * 1000), daysSinceFirstLearn: data[data.length - 1].daysSinceFirstLearn + step, @@ -138,7 +137,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) { }); } daysSinceFirstLearn += totalDaysSinceLastReview; - const retrievability = forgettingCurve(lastStability, totalDaysSinceLastReview); + const retrievability = forgettingCurve(lastStability, totalDaysSinceLastReview, decay); data.push({ date: new Date(now * 1000), daysSinceFirstLearn: daysSinceFirstLearn, @@ -151,7 +150,7 @@ export function prepareData(revlog: RevlogEntry[], maxDays: number) { let previewDaysElapsed = 0; while (previewDaysElapsed < previewDays) { previewDaysElapsed += step; - const retrievability = forgettingCurve(lastStability, elapsedDays + previewDaysElapsed); + const retrievability = forgettingCurve(lastStability, elapsedDays + previewDaysElapsed, decay); data.push({ date: new Date((now + previewDaysElapsed * 86400) * 1000), daysSinceFirstLearn: data[data.length - 1].daysSinceFirstLearn + step, @@ -185,7 +184,9 @@ export function renderForgettingCurve( svgElem: SVGElement, bounds: GraphBounds, desiredRetention: number, + decay: number, ) { + console.log("decay", decay); const svg = select(svgElem); const trans = svg.transition().duration(600) as any; if (filteredRevlog.length === 0) { @@ -194,7 +195,7 @@ export function renderForgettingCurve( } const maxDays = calculateMaxDays(filteredRevlog, timeRange); - const data = prepareData(filteredRevlog, maxDays); + const data = prepareData(filteredRevlog, maxDays, decay); if (data.length === 0) { setDataAvailable(svg, false); diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 224c8de051d..a0b49e5f75c 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -59,7 +59,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: computing = computingParams || checkingParams || computingRetention; $: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`; $: roundedRetention = Number($config.desiredRetention.toFixed(2)); - $: desiredRetentionWarning = getRetentionWarning(roundedRetention); + $: desiredRetentionWarning = getRetentionWarning( + roundedRetention, + fsrsParams($config), + ); $: retentionWarningClass = getRetentionWarningClass(roundedRetention); let computeRetentionProgress: @@ -89,8 +92,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html reviewOrder: $config.reviewOrder, }); - function getRetentionWarning(retention: number): string { - const decay = -0.5; + function getRetentionWarning(retention: number, params: number[]): string { + const decay = params.length > 20 ? -params[20] : -0.5; const factor = 0.9 ** (1 / decay) - 1; const stability = 100; const days = Math.round( From 5baeb03377245ccb3d6918106919c871ca6da188 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 17 Apr 2025 18:08:39 +0800 Subject: [PATCH 04/12] ./ninja fix:minilints --- Cargo.lock | 1 + Cargo.toml | 6 ++-- cargo/licenses.json | 80 +++++++++++++++++++++++++++------------------ 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49792374146..bd4930c7590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2100,6 +2100,7 @@ dependencies = [ [[package]] name = "fsrs" version = "3.0.0" +source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=c15f9783c31db84e6b7ad5d9f30993af734e0c76#c15f9783c31db84e6b7ad5d9f30993af734e0c76" dependencies = [ "burn", "itertools 0.14.0", diff --git a/Cargo.toml b/Cargo.toml index 62e99e08c50..8765fe381b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,9 +36,9 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] # version = "=2.0.3" -# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -# rev = "fc787fe176975d360e8a7ae0c6f5fc9ffadccd16" -path = "../open-spaced-repetition/fsrs-rs" +git = "https://github.com/open-spaced-repetition/fsrs-rs.git" +rev = "c15f9783c31db84e6b7ad5d9f30993af734e0c76" +# path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] # local diff --git a/cargo/licenses.json b/cargo/licenses.json index 243d43afc10..441aaed9493 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -334,7 +334,7 @@ }, { "name": "bincode", - "version": "2.0.0-rc.3", + "version": "2.0.1", "authors": "Ty Overby |Zoey Riordan |Victor Koenders ", "repository": "https://github.com/bincode-org/bincode", "license": "MIT", @@ -415,7 +415,7 @@ }, { "name": "burn", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn", "license": "Apache-2.0 OR MIT", @@ -424,7 +424,7 @@ }, { "name": "burn-autodiff", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-autodiff", "license": "Apache-2.0 OR MIT", @@ -433,7 +433,7 @@ }, { "name": "burn-candle", - "version": "0.16.0", + "version": "0.16.1", "authors": "louisfd ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-candle", "license": "Apache-2.0 OR MIT", @@ -442,7 +442,7 @@ }, { "name": "burn-common", - "version": "0.16.0", + "version": "0.16.1", "authors": "Dilshod Tadjibaev (@antimora)", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-common", "license": "Apache-2.0 OR MIT", @@ -451,7 +451,7 @@ }, { "name": "burn-core", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-core", "license": "Apache-2.0 OR MIT", @@ -460,7 +460,7 @@ }, { "name": "burn-cuda", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-cuda", "license": "Apache-2.0 OR MIT", @@ -469,7 +469,7 @@ }, { "name": "burn-dataset", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-dataset", "license": "Apache-2.0 OR MIT", @@ -478,7 +478,7 @@ }, { "name": "burn-derive", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-derive", "license": "Apache-2.0 OR MIT", @@ -487,7 +487,7 @@ }, { "name": "burn-hip", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-hip", "license": "Apache-2.0 OR MIT", @@ -496,7 +496,7 @@ }, { "name": "burn-jit", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-jit", "license": "Apache-2.0 OR MIT", @@ -505,7 +505,7 @@ }, { "name": "burn-ndarray", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-ndarray", "license": "Apache-2.0 OR MIT", @@ -514,7 +514,7 @@ }, { "name": "burn-router", - "version": "0.16.0", + "version": "0.16.1", "authors": "guillaumelagrange |nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-router", "license": "Apache-2.0 OR MIT", @@ -523,7 +523,7 @@ }, { "name": "burn-tensor", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-tensor", "license": "Apache-2.0 OR MIT", @@ -532,7 +532,7 @@ }, { "name": "burn-train", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-train", "license": "Apache-2.0 OR MIT", @@ -541,7 +541,7 @@ }, { "name": "burn-wgpu", - "version": "0.16.0", + "version": "0.16.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-wgpu", "license": "Apache-2.0 OR MIT", @@ -2071,7 +2071,7 @@ }, { "name": "itertools", - "version": "0.12.1", + "version": "0.13.0", "authors": "bluss", "repository": "https://github.com/rust-itertools/itertools", "license": "Apache-2.0 OR MIT", @@ -2080,7 +2080,7 @@ }, { "name": "itertools", - "version": "0.13.0", + "version": "0.14.0", "authors": "bluss", "repository": "https://github.com/rust-itertools/itertools", "license": "Apache-2.0 OR MIT", @@ -2429,15 +2429,6 @@ "license_file": null, "description": "A wrapper over a platform's native TLS implementation" }, - { - "name": "ndarray", - "version": "0.15.6", - "authors": "Ulrik Sverdrup \"bluss\"|Jim Turner", - "repository": "https://github.com/rust-ndarray/ndarray", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "An n-dimensional array for general elements and for numerics. Lightweight array views and slicing; views support chunking and splitting." - }, { "name": "ndarray", "version": "0.16.1", @@ -2449,7 +2440,7 @@ }, { "name": "ndarray-rand", - "version": "0.14.0", + "version": "0.15.0", "authors": "bluss", "repository": "https://github.com/rust-ndarray/ndarray", "license": "Apache-2.0 OR MIT", @@ -2971,7 +2962,7 @@ }, { "name": "priority-queue", - "version": "2.1.1", + "version": "2.3.1", "authors": "Gianmarco Garrisi ", "repository": "https://github.com/garro95/priority-queue", "license": "LGPL-3.0-or-later OR MPL-2.0", @@ -3574,7 +3565,7 @@ }, { "name": "serde", - "version": "1.0.217", + "version": "1.0.219", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/serde", "license": "Apache-2.0 OR MIT", @@ -3601,7 +3592,7 @@ }, { "name": "serde_derive", - "version": "1.0.217", + "version": "1.0.219", "authors": "Erick Tryzelaar |David Tolnay ", "repository": "https://github.com/serde-rs/serde", "license": "Apache-2.0 OR MIT", @@ -3851,6 +3842,15 @@ "license_file": null, "description": "Helpful macros for working with enums and strings" }, + { + "name": "strum", + "version": "0.27.1", + "authors": "Peter Glotfelty ", + "repository": "https://github.com/Peternator7/strum", + "license": "MIT", + "license_file": null, + "description": "Helpful macros for working with enums and strings" + }, { "name": "strum_macros", "version": "0.26.4", @@ -3860,6 +3860,15 @@ "license_file": null, "description": "Helpful macros for working with enums and strings" }, + { + "name": "strum_macros", + "version": "0.27.1", + "authors": "Peter Glotfelty ", + "repository": "https://github.com/Peternator7/strum", + "license": "MIT", + "license_file": null, + "description": "Helpful macros for working with enums and strings" + }, { "name": "subtle", "version": "2.6.1", @@ -4427,6 +4436,15 @@ "license_file": null, "description": "Safe, fast, zero-panic, zero-crashing, zero-allocation parsing of untrusted inputs in Rust." }, + { + "name": "unty", + "version": "0.0.4", + "authors": "Victor Koenders ", + "repository": "https://github.com/bincode-org/unty", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Explicitly types your generics" + }, { "name": "url", "version": "2.5.4", From 276341f493c9e2c25e36fea0aa5bbf907eb2f36b Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 17 Apr 2025 18:25:12 +0800 Subject: [PATCH 05/12] pass check --- rslib/src/deckconfig/schema11.rs | 1 + ts/routes/card-info/CardInfo.svelte | 16 ++++++++++------ ts/routes/card-info/Revlog.svelte | 2 +- yarn.lock | 6 +++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index 2d825d4ef91..2d862a3a0c0 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -536,6 +536,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! { "interdayLearningMix", "newGatherPriority", "fsrsWeights", + "fsrsParams5", "fsrsParams6", "desiredRetention", "stopTimerOnAnswer", diff --git a/ts/routes/card-info/CardInfo.svelte b/ts/routes/card-info/CardInfo.svelte index cfb62da32ac..80c6daa8ea8 100644 --- a/ts/routes/card-info/CardInfo.svelte +++ b/ts/routes/card-info/CardInfo.svelte @@ -18,12 +18,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: fsrsEnabled = stats?.memoryState != null; $: desiredRetention = stats?.desiredRetention ?? 0.9; - $: decay = - (stats?.fsrsParams?.length ?? 0) === 0 - ? 0.2 - : (stats?.fsrsParams?.length ?? 0) < 21 - ? 0.5 - : (stats?.fsrsParams?.[20] ?? 0.2); + $: decay = (() => { + const paramsLength = stats?.fsrsParams?.length ?? 0; + if (paramsLength === 0) { + return 0.2; + } + if (paramsLength < 21) { + return 0.5; + } + return stats?.fsrsParams?.[20] ?? 0.2; + })(); diff --git a/ts/routes/card-info/Revlog.svelte b/ts/routes/card-info/Revlog.svelte index b02caa3de22..02e9b12f435 100644 --- a/ts/routes/card-info/Revlog.svelte +++ b/ts/routes/card-info/Revlog.svelte @@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { filterRevlogEntryByReviewKind } from "./forgetting-curve"; export let revlog: RevlogEntry[]; - export let fsrsEnabled: boolean = false; + export const fsrsEnabled: boolean = false; function reviewKindClass(entry: RevlogEntry): string { switch (entry.reviewKind) { diff --git a/yarn.lock b/yarn.lock index 76201b219f7..55397df5ec9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2392,9 +2392,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001431, caniuse-lite@npm:^1.0.30001524, caniuse-lite@npm:^1.0.30001669": - version: 1.0.30001671 - resolution: "caniuse-lite@npm:1.0.30001671" - checksum: 10c0/9bb81be7be641fdcdf4d3722b661d4204cc203a489c16080503a72b1605bd5c1061f8ae2452cc6c15d6957c818182824eb34e6569521051795f42cd14e844f99 + version: 1.0.30001714 + resolution: "caniuse-lite@npm:1.0.30001714" + checksum: 10c0/b0e3372f018c5c177912f0282af98049057d83c80846293a4e3df728644a622db42a9e8971d6b7708d76e0fd4e9f6d5ce93802cf4e6818de80fdf371dc0f6a06 languageName: node linkType: hard From 7d9fa06eed5e0eb306c93c7e17a25260c202554c Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 17 Apr 2025 18:41:59 +0800 Subject: [PATCH 06/12] fix NaN in evaluation --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd4930c7590..21b7d543d3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2100,7 +2100,7 @@ dependencies = [ [[package]] name = "fsrs" version = "3.0.0" -source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=c15f9783c31db84e6b7ad5d9f30993af734e0c76#c15f9783c31db84e6b7ad5d9f30993af734e0c76" +source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=fcbd45a6d49b27a8d1764af528560bfe10c2ff9c#fcbd45a6d49b27a8d1764af528560bfe10c2ff9c" dependencies = [ "burn", "itertools 0.14.0", diff --git a/Cargo.toml b/Cargo.toml index 8765fe381b7..5e7e443af05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] # version = "=2.0.3" git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -rev = "c15f9783c31db84e6b7ad5d9f30993af734e0c76" +rev = "fcbd45a6d49b27a8d1764af528560bfe10c2ff9c" # path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] From 8e0c05031e2232c9266754f447f8589b4f6b7290 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 17 Apr 2025 18:45:32 +0800 Subject: [PATCH 07/12] remove console --- ts/routes/card-info/forgetting-curve.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/routes/card-info/forgetting-curve.ts b/ts/routes/card-info/forgetting-curve.ts index 817feccb2b7..71403295841 100644 --- a/ts/routes/card-info/forgetting-curve.ts +++ b/ts/routes/card-info/forgetting-curve.ts @@ -186,7 +186,6 @@ export function renderForgettingCurve( desiredRetention: number, decay: number, ) { - console.log("decay", decay); const svg = select(svgElem); const trans = svg.transition().duration(600) as any; if (filteredRevlog.length === 0) { From fa570bf6e8f4cfc703bec11e702a1de83ad73132 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Fri, 18 Apr 2025 11:09:27 +0800 Subject: [PATCH 08/12] decay should fallback to 0.5 when it's None. --- rslib/src/stats/card.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 74e22295207..539f9f75f05 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -37,7 +37,7 @@ impl Collection { let fsrs_retrievability = card .memory_state .zip(Some(days_elapsed)) - .zip(card.decay) + .zip(Some(card.decay.unwrap_or(0.5))) .map(|((state, days), decay)| { FSRS::new(None) .unwrap() From 991696448280aaccb95cf5fff868acfb7cecbc4f Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Fri, 18 Apr 2025 12:32:46 +0800 Subject: [PATCH 09/12] Update SimulatorModal.svelte --- ts/routes/deck-options/SimulatorModal.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index 1b587c98540..afd0d1eb23b 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -178,7 +178,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ); } - let easyDayPercentages = [...$config.easyDaysPercentages]; + $: easyDayPercentages = [...$config.easyDaysPercentages];