Feat/fsrs simulator backend part (#3075)

* [WIP] FSRS simulator

* add desired_retention as input

* cargo fmt

* fix format

* add standard copyright header

* support existing cards

* fix format

* pass days_elapsed into Card::convert & return None
This commit is contained in:
Jarrett Ye 2024-03-18 21:42:38 +08:00 committed by GitHub
parent bbfe83a8d3
commit 8d197a1555
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 144 additions and 12 deletions

4
Cargo.lock generated
View File

@ -1792,9 +1792,9 @@ dependencies = [
[[package]]
name = "fsrs"
version = "0.5.4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eca50c5f619d6fe0e00962be6f68bf45d67de2fa211a12645882619f5900ff3"
checksum = "84a04c31041078628c5ce7310be96c987bf7f33a3f8815fa0fcdb084eb31feba"
dependencies = [
"burn",
"itertools 0.12.1",

View File

@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs]
version = "0.5.4"
version = "0.5.5"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
# path = "../open-spaced-repetition/fsrs-rs"

View File

@ -1198,7 +1198,7 @@
},
{
"name": "fsrs",
"version": "0.5.4",
"version": "0.5.5",
"authors": "Open Spaced Repetition",
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
"license": "BSD-3-Clause",

View File

@ -51,6 +51,8 @@ service SchedulerService {
returns (GetOptimalRetentionParametersResponse);
rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest)
returns (ComputeOptimalRetentionResponse);
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
returns (SimulateFsrsReviewResponse);
rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse);
rpc ComputeMemoryState(cards.CardId) returns (ComputeMemoryStateResponse);
// The number of days the calculated interval was fuzzed by on the previous
@ -371,6 +373,24 @@ message FsrsReview {
uint32 delta_t = 2;
}
message SimulateFsrsReviewRequest {
repeated float weights = 1;
float desired_retention = 2;
uint32 deck_size = 3;
uint32 days_to_simulate = 4;
uint32 new_limit = 5;
uint32 review_limit = 6;
uint32 max_interval = 7;
string search = 8;
}
message SimulateFsrsReviewResponse {
repeated float accumulated_knowledge_acquisition = 1;
repeated uint32 daily_review_count = 2;
repeated uint32 daily_new_count = 3;
repeated float daily_time_cost = 4;
}
message ComputeOptimalRetentionRequest {
repeated float weights = 1;
uint32 days_to_simulate = 2;

View File

@ -3,5 +3,6 @@
mod error;
pub mod memory_state;
pub mod retention;
pub mod simulator;
pub mod try_collect;
pub mod weights;

View File

@ -8,6 +8,7 @@ use fsrs::FSRS;
use itertools::Itertools;
use crate::prelude::*;
use crate::revlog::RevlogEntry;
use crate::revlog::RevlogReviewKind;
use crate::search::SortMode;
@ -27,7 +28,12 @@ impl Collection {
if req.days_to_simulate == 0 {
invalid_input!("no days to simulate")
}
let p = self.get_optimal_retention_parameters(&req.search)?;
let revlogs = self
.search_cards_into_table(&req.search, SortMode::NoOrder)?
.col
.storage
.get_revlog_entries_for_searched_cards_in_card_order()?;
let p = self.get_optimal_retention_parameters(revlogs)?;
let learn_span = req.days_to_simulate as usize;
let learn_limit = 10;
let deck_size = learn_span * learn_limit;
@ -71,13 +77,8 @@ impl Collection {
pub fn get_optimal_retention_parameters(
&mut self,
search: &str,
revlogs: Vec<RevlogEntry>,
) -> Result<OptimalRetentionParameters> {
let revlogs = self
.search_cards_into_table(search, SortMode::NoOrder)?
.col
.storage
.get_revlog_entries_for_searched_cards_in_card_order()?;
let first_rating_count = revlogs
.iter()
.group_by(|r| r.cid)

View File

@ -0,0 +1,95 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anki_proto::scheduler::SimulateFsrsReviewRequest;
use anki_proto::scheduler::SimulateFsrsReviewResponse;
use fsrs::simulate;
use fsrs::SimulatorConfig;
use itertools::Itertools;
use crate::prelude::*;
use crate::search::SortMode;
impl Collection {
pub fn simulate_review(
&mut self,
req: SimulateFsrsReviewRequest,
) -> Result<SimulateFsrsReviewResponse> {
let guard = self.search_cards_into_table(&req.search, SortMode::NoOrder)?;
let revlogs = guard
.col
.storage
.get_revlog_entries_for_searched_cards_in_card_order()?;
let cards = guard.col.storage.all_searched_cards()?;
drop(guard);
let p = self.get_optimal_retention_parameters(revlogs)?;
let config = SimulatorConfig {
deck_size: req.deck_size as usize,
learn_span: req.days_to_simulate as usize,
max_cost_perday: f64::MAX,
max_ivl: req.max_interval as f64,
recall_costs: [p.recall_secs_hard, p.recall_secs_good, p.recall_secs_easy],
forget_cost: p.forget_secs,
learn_cost: p.learn_secs,
first_rating_prob: [
p.first_rating_probability_again,
p.first_rating_probability_hard,
p.first_rating_probability_good,
p.first_rating_probability_easy,
],
review_rating_prob: [
p.review_rating_probability_hard,
p.review_rating_probability_good,
p.review_rating_probability_easy,
],
loss_aversion: 1.0,
learn_limit: req.new_limit as usize,
review_limit: req.review_limit as usize,
};
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
let (
accumulated_knowledge_acquisition,
daily_review_count,
daily_new_count,
daily_time_cost,
) = simulate(
&config,
&req.weights.iter().map(|w| *w as f64).collect_vec(),
req.desired_retention as f64,
None,
Some(
cards
.into_iter()
.filter_map(|c| Card::convert(c, days_elapsed))
.collect_vec(),
),
);
Ok(SimulateFsrsReviewResponse {
accumulated_knowledge_acquisition: accumulated_knowledge_acquisition
.iter()
.map(|x| *x as f32)
.collect_vec(),
daily_review_count: daily_review_count.iter().map(|x| *x as u32).collect_vec(),
daily_new_count: daily_new_count.iter().map(|x| *x as u32).collect_vec(),
daily_time_cost: daily_time_cost.iter().map(|x| *x as f32).collect_vec(),
})
}
}
impl Card {
fn convert(card: Card, days_elapsed: i32) -> Option<fsrs::Card> {
match card.memory_state {
Some(state) => {
let due = card.original_or_current_due();
let relative_due = due - days_elapsed;
Some(fsrs::Card {
difficulty: state.difficulty as f64,
stability: state.stability as f64,
last_date: (relative_due - card.interval as i32) as f64,
due: relative_due as f64,
})
}
None => None,
}
}
}

View File

@ -15,6 +15,8 @@ use anki_proto::scheduler::FsrsBenchmarkResponse;
use anki_proto::scheduler::FuzzDeltaRequest;
use anki_proto::scheduler::FuzzDeltaResponse;
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
use anki_proto::scheduler::SimulateFsrsReviewRequest;
use anki_proto::scheduler::SimulateFsrsReviewResponse;
use fsrs::FSRSItem;
use fsrs::FSRSReview;
use fsrs::FSRS;
@ -24,6 +26,7 @@ use crate::prelude::*;
use crate::scheduler::new::NewCardDueOrder;
use crate::scheduler::states::CardState;
use crate::scheduler::states::SchedulingStates;
use crate::search::SortMode;
use crate::stats::studied_today;
impl crate::services::SchedulerService for Collection {
@ -264,6 +267,13 @@ impl crate::services::SchedulerService for Collection {
)
}
fn simulate_fsrs_review(
&mut self,
input: SimulateFsrsReviewRequest,
) -> Result<SimulateFsrsReviewResponse> {
self.simulate_review(input)
}
fn compute_optimal_retention(
&mut self,
input: ComputeOptimalRetentionRequest,
@ -292,7 +302,12 @@ impl crate::services::SchedulerService for Collection {
&mut self,
input: scheduler::GetOptimalRetentionParametersRequest,
) -> Result<scheduler::GetOptimalRetentionParametersResponse> {
self.get_optimal_retention_parameters(&input.search)
let revlogs = self
.search_cards_into_table(&input.search, SortMode::NoOrder)?
.col
.storage
.get_revlog_entries_for_searched_cards_in_card_order()?;
self.get_optimal_retention_parameters(revlogs)
.map(|params| GetOptimalRetentionParametersResponse {
params: Some(params),
})