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]]
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",

View file

@ -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]

View file

@ -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

View file

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

View file

@ -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) {

View file

@ -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<String, Value>,
@ -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<DeckConfSchema11> 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<DeckConfig> 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! {

View file

@ -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

View file

@ -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

View file

@ -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::<ComputeMemoryProgress>();
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<FsrsItemWithStartingState>,
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<RevlogEntry>,
next_day_at: TimestampSecs,
sm2_retention: f32,
) -> Vec<(CardId, Option<FsrsItemWithStartingState>)> {
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<RevlogEntry>,
next_day_at: TimestampSecs,
sm2_retention: f32,
) -> Option<FsrsItemWithStartingState> {
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(

View file

@ -214,6 +214,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SettingTitle>
</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">
<WeightsInputRow
bind:value={$config.fsrsWeights}