Fix/FSRS simulator fallback to memory_state_from_sm2 when converting cards (#4189)

* Fix/FSRS simulator fallback to memory_state_from_sm2 for after setting “Ignore cards reviewed before”

* add comment to fsrs_item_for_memory_state

* Add historical retention field to FSRS review request and update related logic

- Added `historical_retention` field to `SimulateFsrsReviewRequest` in `scheduler.proto`.
- Updated `simulator.rs` to use `req.historical_retention` instead of the removed `desired_retention`.
- Modified `FsrsOptions.svelte` to include `historicalRetention` in the options passed to the component.

* Update rslib/src/scheduler/fsrs/memory_state.rs

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* Update rslib/src/scheduler/fsrs/simulator.rs

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* pass ci

* Update rslib/src/scheduler/fsrs/simulator.rs

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* format

* Update rslib/src/scheduler/fsrs/simulator.rs

Co-authored-by: Luc Mcgrady <lucmcgrady@gmail.com>

* format

* Fix condition in is_included_card function to check CardType instead of CardQueue

---------

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
Co-authored-by: Luc Mcgrady <lucmcgrady@gmail.com>
This commit is contained in:
Jarrett Ye 2025-07-09 17:22:59 +08:00 committed by GitHub
parent 208729fa3e
commit 1f7f7bc8a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 53 additions and 36 deletions

View file

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

View file

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

View file

@ -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<fsrs::Card> {
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<fsrs::Card> {
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,
}
}
}

View file

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