diff --git a/proto/anki/cards.proto b/proto/anki/cards.proto index 46a93878f..17e5c3412 100644 --- a/proto/anki/cards.proto +++ b/proto/anki/cards.proto @@ -50,6 +50,7 @@ message Card { uint32 flags = 17; optional uint32 original_position = 18; optional FsrsMemoryState memory_state = 20; + optional float desired_retention = 21; string custom_data = 19; } diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index aca199751..8d06cbe65 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -46,6 +46,7 @@ class Card(DeprecatedNamesMixin): queue: CardQueue type: CardType memory_state: FSRSMemoryState | None + desired_retention: float | None def __init__( self, @@ -96,6 +97,9 @@ class Card(DeprecatedNamesMixin): ) self.custom_data = card.custom_data self.memory_state = card.memory_state if card.HasField("memory_state") else None + self.desired_retention = ( + card.desired_retention if card.HasField("desired_retention") else None + ) def _to_backend_card(self) -> cards_pb2.Card: # mtime & usn are set by backend @@ -118,6 +122,7 @@ class Card(DeprecatedNamesMixin): original_position=self.original_position, custom_data=self.custom_data, memory_state=self.memory_state, + desired_retention=self.desired_retention, ) def flush(self) -> None: diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index 9285993d5..a76ce0e60 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -94,6 +94,7 @@ pub struct Card { /// The position in the new queue before leaving it. pub(crate) original_position: Option, pub(crate) memory_state: Option, + pub(crate) desired_retention: Option, /// JSON object or empty; exposed through the reviewer for persisting custom /// state pub(crate) custom_data: String, @@ -143,6 +144,7 @@ impl Default for Card { flags: 0, original_position: None, memory_state: None, + desired_retention: None, custom_data: String::new(), } } diff --git a/rslib/src/card/service.rs b/rslib/src/card/service.rs index 57efbe8a5..6a43d5312 100644 --- a/rslib/src/card/service.rs +++ b/rslib/src/card/service.rs @@ -101,6 +101,7 @@ impl TryFrom for Card { flags: c.flags as u8, original_position: c.original_position, memory_state: c.memory_state.map(Into::into), + desired_retention: c.desired_retention, custom_data: c.custom_data, }) } @@ -128,6 +129,7 @@ impl From for anki_proto::cards::Card { flags: c.flags as u32, original_position: c.original_position.map(Into::into), memory_state: c.memory_state.map(Into::into), + desired_retention: c.desired_retention, custom_data: c.custom_data, } } diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index 24cda6c57..2ddfe59fa 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -15,7 +15,7 @@ use anki_proto::decks::deck::normal::DayLimit; use crate::config::StringKey; use crate::decks::NormalDeck; use crate::prelude::*; -use crate::scheduler::fsrs::weights::Weights; +use crate::scheduler::fsrs::memory_state::WeightsAndDesiredRetention; use crate::search::JoinSearches; use crate::search::SearchNode; @@ -216,19 +216,20 @@ impl Collection { } if !decks_needing_memory_recompute.is_empty() { - let input: Vec<(Option, Vec)> = decks_needing_memory_recompute - .into_iter() - .map(|(conf_id, search)| { - let weights = configs_after_update.get(&conf_id).and_then(|c| { - if input.fsrs { - Some(c.inner.fsrs_weights.clone()) - } else { - None - } - }); - Ok((weights, search)) - }) - .collect::>()?; + let input: Vec<(Option, Vec)> = + decks_needing_memory_recompute + .into_iter() + .map(|(conf_id, search)| { + let weights = configs_after_update.get(&conf_id).and_then(|c| { + if input.fsrs { + Some((c.inner.fsrs_weights.clone(), c.inner.desired_retention)) + } else { + None + } + }); + Ok((weights, search)) + }) + .collect::>()?; self.update_memory_state(input)?; } diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index f191f11ed..bb01da70c 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -68,6 +68,8 @@ struct CardStateUpdater { fuzz_seed: Option, /// Set if FSRS is enabled. fsrs_next_states: Option, + /// Set if FSRS is enabled. + desired_retention: Option, } impl CardStateUpdater { @@ -159,6 +161,7 @@ impl CardStateUpdater { ) -> RevlogEntryPartial { self.card.reps += 1; self.card.original_due = 0; + self.card.desired_retention = self.desired_retention; let revlog = match next { NormalState::New(next) => self.apply_new_state(current, next), @@ -351,7 +354,8 @@ impl Collection { .get_deck(card.deck_id)? .or_not_found(card.deck_id)?; let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; - let fsrs_next_states = if self.get_config_bool(BoolKey::Fsrs) { + let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs); + let fsrs_next_states = if fsrs_enabled { let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; let memory_state = if let Some(state) = card.memory_state { Some(MemoryState::from(state)) @@ -373,7 +377,7 @@ impl Collection { } else { None }; - + let desired_retention = fsrs_enabled.then_some(config.inner.desired_retention); Ok(CardStateUpdater { fuzz_seed: get_fuzz_seed(&card), card, @@ -382,6 +386,7 @@ impl Collection { timing, now: TimestampSecs::now(), fsrs_next_states, + desired_retention, }) } diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index bfca1e7ce..3e9134d9f 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -17,6 +17,8 @@ pub struct ComputeMemoryProgress { pub total_cards: u32, } +pub(crate) type WeightsAndDesiredRetention = (Weights, f32); + impl Collection { /// For each provided set of weights, locate cards with the provided search, /// and update their memory state. @@ -25,27 +27,30 @@ impl Collection { /// memory state should be removed. pub(crate) fn update_memory_state( &mut self, - entries: Vec<(Option, Vec)>, + entries: Vec<(Option, Vec)>, ) -> Result<()> { let timing = self.timing_today()?; let usn = self.usn()?; - for (weights, search) in entries { + for (weights_and_desired_retention, search) in entries { let search = SearchBuilder::any(search.into_iter()) .and(SearchNode::State(StateKind::New).negated()); let revlog = self.revlog_for_srs(search)?; let items = fsrs_items_for_memory_state(revlog, timing.next_day_at); - let fsrs = FSRS::new(weights.as_deref())?; + let desired_retention = weights_and_desired_retention.as_ref().map(|w| w.1); + let fsrs = FSRS::new(weights_and_desired_retention.as_ref().map(|w| &w.0[..]))?; let mut progress = self.new_progress_handler::(); progress.update(false, |s| s.total_cards = items.len() as u32)?; for (idx, (card_id, item)) in items.into_iter().enumerate() { progress.update(true, |state| state.current_cards = idx as u32 + 1)?; let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?; let original = card.clone(); - if weights.is_some() { + if weights_and_desired_retention.is_some() { let state = fsrs.memory_state(item); card.memory_state = Some(state.into()); + card.desired_retention = desired_retention; } else { card.memory_state = None; + card.desired_retention = None; } self.update_card_inner(&mut card, original, usn)?; } diff --git a/rslib/src/storage/card/data.rs b/rslib/src/storage/card/data.rs index 86c13d7c0..a9151b89d 100644 --- a/rslib/src/storage/card/data.rs +++ b/rslib/src/storage/card/data.rs @@ -38,6 +38,12 @@ pub(crate) struct CardData { deserialize_with = "default_on_invalid" )] pub(crate) fsrs_difficulty: Option, + #[serde( + rename = "dr", + skip_serializing_if = "Option::is_none", + deserialize_with = "default_on_invalid" + )] + pub(crate) fsrs_desired_retention: Option, /// A string representation of a JSON object storing optional data /// associated with the card, so v3 custom scheduling code can persist @@ -52,6 +58,7 @@ impl CardData { original_position: card.original_position, 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, custom_data: card.custom_data.clone(), } } diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 8ca7e30ed..f9da74d05 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -81,6 +81,7 @@ fn row_to_card(row: &Row) -> result::Result { flags: row.get(16)?, original_position: data.original_position, memory_state: data.memory_state(), + desired_retention: data.fsrs_desired_retention, custom_data: data.custom_data, }) } diff --git a/rslib/src/sync/collection/chunks.rs b/rslib/src/sync/collection/chunks.rs index 40720aaaa..c840decf3 100644 --- a/rslib/src/sync/collection/chunks.rs +++ b/rslib/src/sync/collection/chunks.rs @@ -331,6 +331,7 @@ impl From for Card { flags: e.flags, original_position: data.original_position, memory_state: data.memory_state(), + desired_retention: data.fsrs_desired_retention, custom_data: data.custom_data, } }