diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 01f092a39..1294b4543 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -404,6 +404,7 @@ message SimulateFsrsReviewRequest { repeated float easy_days_percentages = 10; deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11; optional uint32 suspend_after_lapse_count = 12; + float historical_retention = 13; } message SimulateFsrsReviewResponse { diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 425d8da69..b2640fa3e 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -377,6 +377,7 @@ pub(crate) fn fsrs_item_for_memory_state( Ok(None) } } else { + // no revlogs (new card or caused by ignore_revlogs_before or deleted revlogs) Ok(None) } } diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 34cc925d6..e032ecaf3 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -10,11 +10,14 @@ use fsrs::simulate; use fsrs::PostSchedulingFn; use fsrs::ReviewPriorityFn; use fsrs::SimulatorConfig; +use fsrs::FSRS; use itertools::Itertools; use rand::rngs::StdRng; use rand::Rng; use crate::card::CardQueue; +use crate::card::CardType; +use crate::card::FsrsMemoryState; use crate::prelude::*; use crate::scheduler::states::fuzz::constrained_fuzz_bounds; use crate::scheduler::states::load_balancer::calculate_easy_days_modifiers; @@ -129,7 +132,7 @@ impl Collection { fn is_included_card(c: &Card) -> bool { c.queue != CardQueue::Suspended && c.queue != CardQueue::PreviewRepeat - && c.queue != CardQueue::New + && c.ctype != CardType::New } // calculate any missing memory state for c in &mut cards { @@ -143,13 +146,29 @@ impl Collection { let days_elapsed = self.timing_today().unwrap().days_elapsed as i32; let new_cards = cards .iter() - .filter(|c| c.memory_state.is_none() || c.queue == CardQueue::New) + .filter(|c| c.ctype == CardType::New && c.queue != CardQueue::Suspended) .count() + req.deck_size as usize; + let fsrs = FSRS::new(Some(&req.params))?; let mut converted_cards = cards .into_iter() .filter(is_included_card) - .filter_map(|c| Card::convert(c, days_elapsed)) + .filter_map(|c| { + let memory_state = match c.memory_state { + Some(state) => state, + // cards that lack memory states after compute_memory_state have no FSRS items, + // implying a truncated or ignored revlog + None => fsrs + .memory_state_from_sm2( + c.ease_factor(), + c.interval as f32, + req.historical_retention, + ) + .ok()? + .into(), + }; + Card::convert(c, days_elapsed, memory_state) + }) .collect_vec(); let introduced_today_count = self .search_cards(&format!("{} introduced:1", &req.search), SortMode::NoOrder)? @@ -251,39 +270,34 @@ impl Collection { } impl Card { - fn convert(card: Card, days_elapsed: i32) -> Option { - match card.memory_state { - Some(state) => match card.queue { - CardQueue::DayLearn | CardQueue::Review => { - let due = card.original_or_current_due(); - let relative_due = due - days_elapsed; - let last_date = (relative_due - card.interval as i32).min(0) as f32; - Some(fsrs::Card { - id: card.id.0, - difficulty: state.difficulty, - stability: state.stability, - last_date, - due: relative_due as f32, - interval: card.interval as f32, - lapses: card.lapses, - }) - } - CardQueue::New => None, - CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => { - Some(fsrs::Card { - id: card.id.0, - difficulty: state.difficulty, - stability: state.stability, - last_date: 0.0, - due: 0.0, - interval: card.interval as f32, - lapses: card.lapses, - }) - } - CardQueue::PreviewRepeat => None, - CardQueue::Suspended => None, - }, - None => None, + fn convert(card: Card, days_elapsed: i32, memory_state: FsrsMemoryState) -> Option { + match card.queue { + CardQueue::DayLearn | CardQueue::Review => { + let due = card.original_or_current_due(); + let relative_due = due - days_elapsed; + let last_date = (relative_due - card.interval as i32).min(0) as f32; + Some(fsrs::Card { + id: card.id.0, + difficulty: memory_state.difficulty, + stability: memory_state.stability, + last_date, + due: relative_due as f32, + interval: card.interval as f32, + lapses: card.lapses, + }) + } + CardQueue::New => None, + CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => Some(fsrs::Card { + id: card.id.0, + difficulty: memory_state.difficulty, + stability: memory_state.stability, + last_date: 0.0, + due: 0.0, + interval: card.interval as f32, + lapses: card.lapses, + }), + CardQueue::PreviewRepeat => None, + CardQueue::Suspended => None, } } } diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 706407889..cfdea341c 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -95,6 +95,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit, easyDaysPercentages: $config.easyDaysPercentages, reviewOrder: $config.reviewOrder, + historicalRetention: $config.historicalRetention, }); const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;