diff --git a/Cargo.lock b/Cargo.lock index a4b74321f..2410395d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1477,7 +1477,7 @@ dependencies = [ [[package]] name = "fsrs" version = "0.1.0" -source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=c34ac397635b6d04fdee07ace9401047f37a5f3e#c34ac397635b6d04fdee07ace9401047f37a5f3e" +source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=5089470af3944f7efbf386be09f3858342d4d5af#5089470af3944f7efbf386be09f3858342d4d5af" dependencies = [ "burn", "itertools 0.11.0", diff --git a/Cargo.toml b/Cargo.toml index c80c559e1..82d6653f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -rev = "c34ac397635b6d04fdee07ace9401047f37a5f3e" +rev = "5089470af3944f7efbf386be09f3858342d4d5af" # path = "../../../fsrs-rs" [workspace.dependencies] diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index df084bec5..2ed47caf6 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -331,6 +331,7 @@ deck-config-optimize-button = Optimize deck-config-compute-button = Compute deck-config-analyze-button = Analyze deck-config-desired-retention = Desired retention +deck-config-sm2-retention = SM2 retention deck-config-smaller-is-better = Smaller numbers indicate a better fit to your review history. deck-config-steps-too-large-for-fsrs = When FSRS is enabled, learning steps over 1 day are not recommended. deck-config-get-params = Get Params diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index f5023c7c6..fe9ba7a50 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -142,6 +142,7 @@ message DeckConfig { // for fsrs float desired_retention = 37; bool reschedule_fsrs_cards = 39; + float sm2_retention = 40; bytes other = 255; } diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index c6858906b..17088344c 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -71,6 +71,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner { desired_retention: 0.9, other: Vec::new(), reschedule_fsrs_cards: false, + sm2_retention: 0.9, }; impl Default for DeckConfig { @@ -275,6 +276,7 @@ pub(crate) fn ensure_deck_config_values_valid(config: &mut DeckConfigInner) { 0.7, 0.97, ); + ensure_f32_valid(&mut config.sm2_retention, default.sm2_retention, 0.7, 0.97) } fn ensure_f32_valid(val: &mut f32, default: f32, min: f32, max: f32) { diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index 539e92af9..ffb83fa69 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -73,6 +73,8 @@ pub struct DeckConfSchema11 { stop_timer_on_answer: bool, #[serde(default)] reschedule_fsrs_cards: bool, + #[serde(default)] + sm2_retention: f32, #[serde(flatten)] other: HashMap, @@ -263,6 +265,7 @@ impl Default for DeckConfSchema11 { fsrs_weights: vec![], desired_retention: 0.9, reschedule_fsrs_cards: false, + sm2_retention: 0.9, } } } @@ -335,6 +338,7 @@ impl From for DeckConfig { fsrs_weights: c.fsrs_weights, desired_retention: c.desired_retention, reschedule_fsrs_cards: c.reschedule_fsrs_cards, + sm2_retention: c.sm2_retention, other: other_bytes, }, } @@ -430,6 +434,7 @@ impl From for DeckConfSchema11 { fsrs_weights: i.fsrs_weights, desired_retention: i.desired_retention, reschedule_fsrs_cards: i.reschedule_fsrs_cards, + sm2_retention: 0.9, } } } @@ -454,7 +459,8 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! { "fsrsWeights", "desiredRetention", "stopTimerOnAnswer", - "rescheduleFsrsCards" + "rescheduleFsrsCards", + "sm2Retention", }; static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! { diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index e5573c538..a619b766b 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -236,6 +236,7 @@ impl Collection { desired_retention: c.inner.desired_retention, max_interval: c.inner.maximum_review_interval, reschedule: c.inner.reschedule_fsrs_cards, + sm2_retention: c.inner.sm2_retention, }) } else { None diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 927d856b3..84caf150a 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -361,8 +361,13 @@ impl Collection { // and will need its initial memory state to be calculated based on review // history. let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?; - let item = single_card_revlog_to_item(&fsrs, revlog, timing.next_day_at); - card.set_memory_state(&fsrs, item); + let item = single_card_revlog_to_item( + &fsrs, + revlog, + timing.next_day_at, + config.inner.sm2_retention, + ); + card.set_memory_state(&fsrs, item, config.inner.sm2_retention); } let days_elapsed = self .storage diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index efc3c0b5c..691c74d9b 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -30,6 +30,7 @@ pub struct ComputeMemoryProgress { pub(crate) struct UpdateMemoryStateRequest { pub weights: Weights, pub desired_retention: f32, + pub sm2_retention: f32, pub max_interval: u32, pub reschedule: bool, } @@ -57,7 +58,13 @@ impl Collection { None }; let fsrs = FSRS::new(req.as_ref().map(|w| &w.weights[..]).or(Some([].as_slice())))?; - let items = fsrs_items_for_memory_state(&fsrs, revlog, timing.next_day_at); + let sm2_retention = req.as_ref().map(|w| w.sm2_retention); + let items = fsrs_items_for_memory_state( + &fsrs, + revlog, + timing.next_day_at, + sm2_retention.unwrap_or(0.9), + ); let desired_retention = req.as_ref().map(|w| w.desired_retention); let mut progress = self.new_progress_handler::(); progress.update(false, |s| s.total_cards = items.len() as u32)?; @@ -66,7 +73,7 @@ impl Collection { let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); if let Some(req) = &req { - card.set_memory_state(&fsrs, item); + card.set_memory_state(&fsrs, item, sm2_retention.unwrap()); card.desired_retention = desired_retention; // if rescheduling if let Some(reviews) = &last_reviews { @@ -127,10 +134,16 @@ impl Collection { .get_deck_config(conf_id)? .or_not_found(conf_id)?; let desired_retention = config.inner.desired_retention; + let sm2_retention = config.inner.sm2_retention; let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?; - let item = single_card_revlog_to_item(&fsrs, revlog, self.timing_today()?.next_day_at); - card.set_memory_state(&fsrs, item); + let item = single_card_revlog_to_item( + &fsrs, + revlog, + self.timing_today()?.next_day_at, + sm2_retention, + ); + card.set_memory_state(&fsrs, item, sm2_retention); Ok(ComputeMemoryStateResponse { state: card.memory_state.map(Into::into), desired_retention, @@ -143,6 +156,7 @@ impl Card { &mut self, fsrs: &FSRS, item: Option, + sm2_retention: f32, ) { self.memory_state = item .map(|i| fsrs.memory_state(i.item, i.starting_state)) @@ -151,7 +165,11 @@ impl Card { None } else { // no valid revlog entries; infer state from current card state - Some(fsrs.memory_state_from_sm2(self.ease_factor(), self.interval as f32)) + Some(fsrs.memory_state_from_sm2( + self.ease_factor(), + self.interval as f32, + sm2_retention, + )) } }) .map(Into::into); @@ -172,6 +190,7 @@ pub(crate) fn fsrs_items_for_memory_state( fsrs: &FSRS, revlogs: Vec, next_day_at: TimestampSecs, + sm2_retention: f32, ) -> Vec<(CardId, Option)> { revlogs .into_iter() @@ -180,7 +199,7 @@ pub(crate) fn fsrs_items_for_memory_state( .map(|(card_id, group)| { ( card_id, - single_card_revlog_to_item(fsrs, group.collect(), next_day_at), + single_card_revlog_to_item(fsrs, group.collect(), next_day_at, sm2_retention), ) }) .collect() @@ -214,6 +233,7 @@ pub(crate) fn single_card_revlog_to_item( fsrs: &FSRS, entries: Vec, next_day_at: TimestampSecs, + sm2_retention: f32, ) -> Option { let have_learning = entries .iter() @@ -232,7 +252,7 @@ pub(crate) fn single_card_revlog_to_item( }; let interval = first_review.interval.max(1); let starting_state = - fsrs.memory_state_from_sm2(ease_factor as f32 / 1000.0, interval as f32); + fsrs.memory_state_from_sm2(ease_factor as f32 / 1000.0, interval as f32, sm2_retention); let items = single_card_revlog_to_items(entries, next_day_at, false); items.and_then(|mut items| { let mut item = items.pop().unwrap(); @@ -277,6 +297,7 @@ mod tests { revlog(RevlogReviewKind::Review, 1), ], TimestampSecs::now(), + 0.9, ) .unwrap(); assert_eq!( @@ -287,7 +308,7 @@ mod tests { }) ); let mut card = Card::default(); - card.set_memory_state(&fsrs, Some(item)); + card.set_memory_state(&fsrs, Some(item), 0.9); assert_eq!( card.memory_state, Some(FsrsMemoryState { @@ -305,12 +326,13 @@ mod tests { ..revlog(RevlogReviewKind::Review, 100) }], TimestampSecs::now(), + 0.9, ); assert!(item.is_none()); card.interval = 123; card.ease_factor = 2000; card.ctype = CardType::Review; - card.set_memory_state(&fsrs, item); + card.set_memory_state(&fsrs, item, 0.9); assert_eq!( card.memory_state, Some(FsrsMemoryState { @@ -331,7 +353,7 @@ mod tests { ease_factor: 1300, ..Default::default() }; - card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None); + card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None, 0.9); assert_eq!( card.memory_state, Some( diff --git a/ts/deck-options/FsrsOptions.svelte b/ts/deck-options/FsrsOptions.svelte index e98744408..878d1b605 100644 --- a/ts/deck-options/FsrsOptions.svelte +++ b/ts/deck-options/FsrsOptions.svelte @@ -214,6 +214,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + + {tr.deckConfigSm2Retention()} + + +