mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
parent
9fd6d86a3e
commit
003cdfd2ec
10 changed files with 64 additions and 15 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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! {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue