Use sm2 retention when deriving memory state

Closes #2702
This commit is contained in:
Damien Elmes 2023-10-13 10:12:04 +10:00
parent 9fd6d86a3e
commit 003cdfd2ec
10 changed files with 64 additions and 15 deletions

2
Cargo.lock generated
View file

@ -1477,7 +1477,7 @@ dependencies = [
[[package]] [[package]]
name = "fsrs" name = "fsrs"
version = "0.1.0" 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 = [ dependencies = [
"burn", "burn",
"itertools 0.11.0", "itertools 0.11.0",

View file

@ -36,7 +36,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs] [workspace.dependencies.fsrs]
git = "https://github.com/open-spaced-repetition/fsrs-rs.git" git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
rev = "c34ac397635b6d04fdee07ace9401047f37a5f3e" rev = "5089470af3944f7efbf386be09f3858342d4d5af"
# path = "../../../fsrs-rs" # path = "../../../fsrs-rs"
[workspace.dependencies] [workspace.dependencies]

View file

@ -331,6 +331,7 @@ deck-config-optimize-button = Optimize
deck-config-compute-button = Compute deck-config-compute-button = Compute
deck-config-analyze-button = Analyze deck-config-analyze-button = Analyze
deck-config-desired-retention = Desired retention 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-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-steps-too-large-for-fsrs = When FSRS is enabled, learning steps over 1 day are not recommended.
deck-config-get-params = Get Params deck-config-get-params = Get Params

View file

@ -142,6 +142,7 @@ message DeckConfig {
// for fsrs // for fsrs
float desired_retention = 37; float desired_retention = 37;
bool reschedule_fsrs_cards = 39; bool reschedule_fsrs_cards = 39;
float sm2_retention = 40;
bytes other = 255; bytes other = 255;
} }

View file

@ -71,6 +71,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
desired_retention: 0.9, desired_retention: 0.9,
other: Vec::new(), other: Vec::new(),
reschedule_fsrs_cards: false, reschedule_fsrs_cards: false,
sm2_retention: 0.9,
}; };
impl Default for DeckConfig { impl Default for DeckConfig {
@ -275,6 +276,7 @@ pub(crate) fn ensure_deck_config_values_valid(config: &mut DeckConfigInner) {
0.7, 0.7,
0.97, 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) { fn ensure_f32_valid(val: &mut f32, default: f32, min: f32, max: f32) {

View file

@ -73,6 +73,8 @@ pub struct DeckConfSchema11 {
stop_timer_on_answer: bool, stop_timer_on_answer: bool,
#[serde(default)] #[serde(default)]
reschedule_fsrs_cards: bool, reschedule_fsrs_cards: bool,
#[serde(default)]
sm2_retention: f32,
#[serde(flatten)] #[serde(flatten)]
other: HashMap<String, Value>, other: HashMap<String, Value>,
@ -263,6 +265,7 @@ impl Default for DeckConfSchema11 {
fsrs_weights: vec![], fsrs_weights: vec![],
desired_retention: 0.9, desired_retention: 0.9,
reschedule_fsrs_cards: false, reschedule_fsrs_cards: false,
sm2_retention: 0.9,
} }
} }
} }
@ -335,6 +338,7 @@ impl From<DeckConfSchema11> for DeckConfig {
fsrs_weights: c.fsrs_weights, fsrs_weights: c.fsrs_weights,
desired_retention: c.desired_retention, desired_retention: c.desired_retention,
reschedule_fsrs_cards: c.reschedule_fsrs_cards, reschedule_fsrs_cards: c.reschedule_fsrs_cards,
sm2_retention: c.sm2_retention,
other: other_bytes, other: other_bytes,
}, },
} }
@ -430,6 +434,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
fsrs_weights: i.fsrs_weights, fsrs_weights: i.fsrs_weights,
desired_retention: i.desired_retention, desired_retention: i.desired_retention,
reschedule_fsrs_cards: i.reschedule_fsrs_cards, 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", "fsrsWeights",
"desiredRetention", "desiredRetention",
"stopTimerOnAnswer", "stopTimerOnAnswer",
"rescheduleFsrsCards" "rescheduleFsrsCards",
"sm2Retention",
}; };
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! { static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {

View file

@ -236,6 +236,7 @@ impl Collection {
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: c.inner.reschedule_fsrs_cards, reschedule: c.inner.reschedule_fsrs_cards,
sm2_retention: c.inner.sm2_retention,
}) })
} else { } else {
None None

View file

@ -361,8 +361,13 @@ impl Collection {
// 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
// history. // history.
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(&fsrs, revlog, timing.next_day_at); let item = single_card_revlog_to_item(
card.set_memory_state(&fsrs, 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 let days_elapsed = self
.storage .storage

View file

@ -30,6 +30,7 @@ pub struct ComputeMemoryProgress {
pub(crate) struct UpdateMemoryStateRequest { pub(crate) struct UpdateMemoryStateRequest {
pub weights: Weights, pub weights: Weights,
pub desired_retention: f32, pub desired_retention: f32,
pub sm2_retention: f32,
pub max_interval: u32, pub max_interval: u32,
pub reschedule: bool, pub reschedule: bool,
} }
@ -57,7 +58,13 @@ impl Collection {
None None
}; };
let fsrs = FSRS::new(req.as_ref().map(|w| &w.weights[..]).or(Some([].as_slice())))?; 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 desired_retention = req.as_ref().map(|w| w.desired_retention);
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>(); let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
progress.update(false, |s| s.total_cards = items.len() as u32)?; 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 mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
let original = card.clone(); let original = card.clone();
if let Some(req) = &req { 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; card.desired_retention = desired_retention;
// if rescheduling // if rescheduling
if let Some(reviews) = &last_reviews { if let Some(reviews) = &last_reviews {
@ -127,10 +134,16 @@ impl Collection {
.get_deck_config(conf_id)? .get_deck_config(conf_id)?
.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 sm2_retention = config.inner.sm2_retention;
let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?;
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(&fsrs, revlog, self.timing_today()?.next_day_at); let item = single_card_revlog_to_item(
card.set_memory_state(&fsrs, item); &fsrs,
revlog,
self.timing_today()?.next_day_at,
sm2_retention,
);
card.set_memory_state(&fsrs, item, sm2_retention);
Ok(ComputeMemoryStateResponse { Ok(ComputeMemoryStateResponse {
state: card.memory_state.map(Into::into), state: card.memory_state.map(Into::into),
desired_retention, desired_retention,
@ -143,6 +156,7 @@ impl Card {
&mut self, &mut self,
fsrs: &FSRS, fsrs: &FSRS,
item: Option<FsrsItemWithStartingState>, item: Option<FsrsItemWithStartingState>,
sm2_retention: f32,
) { ) {
self.memory_state = item self.memory_state = item
.map(|i| fsrs.memory_state(i.item, i.starting_state)) .map(|i| fsrs.memory_state(i.item, i.starting_state))
@ -151,7 +165,11 @@ impl Card {
None None
} else { } else {
// no valid revlog entries; infer state from current card state // 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); .map(Into::into);
@ -172,6 +190,7 @@ pub(crate) fn fsrs_items_for_memory_state(
fsrs: &FSRS, fsrs: &FSRS,
revlogs: Vec<RevlogEntry>, revlogs: Vec<RevlogEntry>,
next_day_at: TimestampSecs, next_day_at: TimestampSecs,
sm2_retention: f32,
) -> Vec<(CardId, Option<FsrsItemWithStartingState>)> { ) -> Vec<(CardId, Option<FsrsItemWithStartingState>)> {
revlogs revlogs
.into_iter() .into_iter()
@ -180,7 +199,7 @@ pub(crate) fn fsrs_items_for_memory_state(
.map(|(card_id, group)| { .map(|(card_id, group)| {
( (
card_id, 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() .collect()
@ -214,6 +233,7 @@ pub(crate) fn single_card_revlog_to_item(
fsrs: &FSRS, fsrs: &FSRS,
entries: Vec<RevlogEntry>, entries: Vec<RevlogEntry>,
next_day_at: TimestampSecs, next_day_at: TimestampSecs,
sm2_retention: f32,
) -> Option<FsrsItemWithStartingState> { ) -> Option<FsrsItemWithStartingState> {
let have_learning = entries let have_learning = entries
.iter() .iter()
@ -232,7 +252,7 @@ pub(crate) fn single_card_revlog_to_item(
}; };
let interval = first_review.interval.max(1); let interval = first_review.interval.max(1);
let starting_state = 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); let items = single_card_revlog_to_items(entries, next_day_at, false);
items.and_then(|mut items| { items.and_then(|mut items| {
let mut item = items.pop().unwrap(); let mut item = items.pop().unwrap();
@ -277,6 +297,7 @@ mod tests {
revlog(RevlogReviewKind::Review, 1), revlog(RevlogReviewKind::Review, 1),
], ],
TimestampSecs::now(), TimestampSecs::now(),
0.9,
) )
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -287,7 +308,7 @@ mod tests {
}) })
); );
let mut card = Card::default(); let mut card = Card::default();
card.set_memory_state(&fsrs, Some(item)); card.set_memory_state(&fsrs, Some(item), 0.9);
assert_eq!( assert_eq!(
card.memory_state, card.memory_state,
Some(FsrsMemoryState { Some(FsrsMemoryState {
@ -305,12 +326,13 @@ mod tests {
..revlog(RevlogReviewKind::Review, 100) ..revlog(RevlogReviewKind::Review, 100)
}], }],
TimestampSecs::now(), TimestampSecs::now(),
0.9,
); );
assert!(item.is_none()); assert!(item.is_none());
card.interval = 123; card.interval = 123;
card.ease_factor = 2000; card.ease_factor = 2000;
card.ctype = CardType::Review; card.ctype = CardType::Review;
card.set_memory_state(&fsrs, item); card.set_memory_state(&fsrs, item, 0.9);
assert_eq!( assert_eq!(
card.memory_state, card.memory_state,
Some(FsrsMemoryState { Some(FsrsMemoryState {
@ -331,7 +353,7 @@ mod tests {
ease_factor: 1300, ease_factor: 1300,
..Default::default() ..Default::default()
}; };
card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None); card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None, 0.9);
assert_eq!( assert_eq!(
card.memory_state, card.memory_state,
Some( Some(

View file

@ -214,6 +214,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SettingTitle> </SettingTitle>
</SpinBoxFloatRow> </SpinBoxFloatRow>
<SpinBoxFloatRow
bind:value={$config.sm2Retention}
defaultValue={defaults.sm2Retention}
min={0.7}
max={0.97}
>
<SettingTitle>
{tr.deckConfigSm2Retention()}
</SettingTitle>
</SpinBoxFloatRow>
<div class="ms-1 me-1"> <div class="ms-1 me-1">
<WeightsInputRow <WeightsInputRow
bind:value={$config.fsrsWeights} bind:value={$config.fsrsWeights}