Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions proto/anki/scheduler.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions rslib/src/scheduler/fsrs/memory_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
89 changes: 50 additions & 39 deletions rslib/src/scheduler/fsrs/simulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -141,15 +144,28 @@ 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)
.count()
+ req.deck_size as usize;
let new_cards =
cards.iter().filter(|c| c.ctype == CardType::New).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
Copy link
Contributor

@user1823 user1823 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we exclude new cards here to improve performance and prevent introduction of new bugs?

Edit: filter(is_included_card) already excludes the new cards. I got confused by CardQueue::New => None, in the convert function. That's not actually needed, IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got confused by CardQueue::New => None, in the convert function. That's not actually needed, IMO.

It's required by Rust's compiler.

.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)?
Expand Down Expand Up @@ -251,39 +267,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,
}
}
}
1 change: 1 addition & 0 deletions ts/routes/deck-options/FsrsOptions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down